Skip to content

feat: normalize provider-bound Responses state - #5410

Open
huangruiteng wants to merge 14 commits into
router-for-me:devfrom
huangruiteng:codex/provider-bound-history-sse-normalizers
Open

feat: normalize provider-bound Responses state#5410
huangruiteng wants to merge 14 commits into
router-for-me:devfrom
huangruiteng:codex/provider-bound-history-sse-normalizers

Conversation

@huangruiteng

Copy link
Copy Markdown

Supersedes #5220, which was auto-closed after its stale base snapshot incorrectly treated newer upstream dev files as part of the pull request.

Summary

  • keep provider-bound Responses history scoped to the selected credential and project mixed histories to provider-neutral content during A/B handoff;
  • preserve native same-credential incremental previous_response_id requests while continuing to quarantine mixed transcripts;
  • recover only a known orphan App-host automation_update output, while unknown or potentially paired missing-call_id outputs still fail closed;
  • reject credential-bound hosted-tool resources during foreign-history replay;
  • keep stable prompt-cache aliases across changing request IDs;
  • preserve complete SSE frames, lifecycle ordering, and refusal content.

Validation

  • gofmt on changed Go files
  • go build -o test-output ./cmd/server
  • go test ./...
  • focused go test -race for provider-history, SSE, and websocket paths
  • git diff --check origin/dev

The replacement compare contains only the intended SDK files and no AGENTS.md or internal/translator changes.

Signed-off-by: huangruiteng <huangrt01@163.com>
…istory-sse-normalizers

Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
…phan-recovery

# Conflicts:
#	sdk/cliproxy/auth/conductor_stream.go
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T04:03:42.771045Z 566da3b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 235af07cd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +147 to +148
if committed {
opts.EnsureMetadata()[cliproxyexecutor.ProviderOutputCommittedMetadataKey] = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clone metadata before marking committed streams

When a stream delivers one payload and then fails, this goroutine writes into the same metadata map originally passed by the handler because Options was only shallow-copied. After the unbuffered first-chunk send completes, the handler may still be iterating or serializing opts.Metadata for a stream interceptor while this code processes the next error chunk, causing a data race and potentially a fatal concurrent-map panic. Give the wrapper private metadata before making this asynchronous write.

AGENTS.md reference: AGENTS.md:L57-L57

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6f1c08c. wrapStreamResult now clones request metadata before starting the goroutine, so committed/error bookkeeping cannot mutate the handler-owned map. Added a regression that drains a payload-then-error stream and verifies the caller map remains unchanged; focused race and full-suite validation pass.

Comment on lines +383 to +385
func providerHistorySessionIDs(headers http.Header, payload []byte, metadata map[string]any) []string {
primaryID, fallbackID := extractSessionIDs(headers, payload, metadata)
aliases := mergeSessionAliases(nil, primaryID, fallbackID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid heuristic message hashes for ownership checks

When a Responses request has no explicit session header, prompt-cache key, or other stable identifier, extractSessionIDs falls back to a hash of the first user text. That heuristic is acceptable for affinity but not for credential-ownership enforcement: two unrelated conversations whose current input is the same common text (for example, continue) become one session, so after one uses credential A, a separate request using its own previous_response_id under credential B is rejected as a foreign continuation. Provider-history safety decisions should use only identifiers that uniquely establish the conversation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6f1c08c. Provider-history ownership now uses only explicit client/execution identities plus the stable prompt-cache alias; the affinity-only first-message hash fallback is excluded. Added a regression proving a common message such as continue produces no ownership key.

Comment thread sdk/cliproxy/auth/provider_history.go Outdated
Comment on lines +147 to +151
for key, item := range typed {
switch strings.ToLower(strings.TrimSpace(key)) {
case "vector_store_id", "vector_store_ids":
if hasSemanticHistoryValue(item) {
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore parameter schemas when detecting bound tool IDs

On a foreign-history replay, this recursive scan treats any semantically nonempty value beneath a key named vector_store_id or vector_store_ids as an actual hosted resource. A normal custom-function schema such as properties.vector_store_id: {"type":"string"} therefore triggers foreign_tool_resource_requires_rehydration, even though it merely declares an argument and supplies no credential-bound ID. Restrict this check to concrete hosted-tool configuration values rather than recursively traversing arbitrary tool schemas.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6f1c08c. Bound-resource detection now inspects only concrete file_search tool configuration fields. It no longer recursively traverses arbitrary function parameter schemas; a custom function property named vector_store_id is covered by a new normalization regression.

@huangruiteng

Copy link
Copy Markdown
Author

@codex review

Replacement for auto-closed #5220. The refreshed compare contains only the intended SDK files. This head also handles the App-host orphan automation_update output conservatively, preserves same-credential native incremental websocket requests, keeps mixed previous-response transcripts quarantined, rejects credential-bound hosted-tool resources, stabilizes prompt-cache session aliases, and preserves complete SSE/refusal frames. Full tests, focused race tests, required build, diff hygiene, and the exact-scope quality gate pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 235af07cd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +427 to +430
case "response.reasoning_summary_text.delta", "response.reasoning_text.delta":
delta, _ := event["delta"].(string)
s.active.summaries[summaryIndex] += delta
case "response.reasoning_summary_text.done", "response.reasoning_text.done":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve reasoning_text in synthesized reasoning items

When a compatible provider omits response.output_item.done but emits response.reasoning_text.delta or .done, these cases accumulate the text as a summary; closeActive consequently serializes it under item.summary as summary_text and emits no content entry of type reasoning_text. This changes the Responses schema semantics and causes clients replaying the synthesized completed item to lose the actual reasoning-content shape, so raw reasoning text should be tracked separately and emitted in item.content.

Useful? React with 👍 / 👎.

@huangruiteng

Copy link
Copy Markdown
Author

@codex review

Addressed all current review findings in 6f1c08c: private stream metadata ownership, explicit-only provider-history identity, and concrete file_search resource detection. Added negative regressions and reran focused/race/full-suite/build/diff validation; exact-scope quality receipt cqr_a3048a2fe9640eb5b484 passes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f1c08c360

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +111 to +115
if itemType == "reasoning" && !hasSemanticHistoryValue(item["summary"]) {
result.Changed = true
result.DroppedItems++
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve reasoning content on credential handoff

When a foreign-history reasoning item has an empty summary but portable text in content entries of type reasoning_text, this condition drops the entire item after removing its provider-bound fields. That is a supported Responses shape—the request translators explicitly read content[].reasoning_text when no summary exists—so a credential handoff silently removes useful reasoning context. Treat semantic reasoning content as sufficient to retain the item.

Useful? React with 👍 / 👎.

Signed-off-by: huangruiteng <huangrt01@163.com>
@huangruiteng

huangruiteng commented Sep 3, 2026

Copy link
Copy Markdown
Author

Correction after live same-task validation: dropping a confirmed orphan host output is syntactically valid but semantically unsafe. In Codex App, the orphan function_call_output can itself carry the new heartbeat/delegation instruction. Removing it prevents the 409 while causing the model to continue stale work instead of following the new instruction.

The updated PR head (566da3b) therefore projects only confirmed orphan host-control outputs into provider-neutral role=user messages:

  • semantic output content is preserved exactly;
  • host tool name and provider-bound item identity are removed;
  • unknown output names remain rejected;
  • an allowlisted name with a matching call but missing call_id remains rejected;
  • empty/non-semantic outputs remain rejected.

Validation:

  • focused provider-history and compatible same-scope projection regressions pass;
  • go test ./... -count=1 passes on the updated PR branch;
  • the integrated self-use build passes isolated routing smoke and Codex App Server selector/readback checks.

This supersedes the earlier wording in this comment that described the orphan output as safe to drop.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 952e2f587f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +64
if len(s.pending) > 0 && !bytes.HasSuffix(s.pending, []byte("\n")) && !bytes.HasPrefix(chunk, []byte("\n")) && responsesSSEChunkStartsField(chunk) {
s.pending = append(s.pending, '\n')
}
s.pending = append(s.pending, chunk...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delimit complete SSE chunks before buffering the next event

When a compatible Responses model uses OpenAICompatExecutor, each translated chunk is emitted without a trailing blank-line delimiter (openai_compat_executor.go constructs streamLine as data: <payload>, and common.SSEEventData likewise omits trailing newlines). This state therefore buffers the first complete event, inserts only one newline before the next field, and never finds \n\n; clients receive no events and Finish() ultimately reports an incomplete stream as a 502. Treat a complete pending data frame followed by a new data:/event: field as a frame boundary rather than merely inserting one newline.

Useful? React with 👍 / 👎.

Signed-off-by: huangruiteng <huangrt01@163.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 566da3b430

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +412 to +417
itemType, _ := items[0]["type"].(string)
if itemType == "message" {
return true
}
_, hasRole := items[0]["role"].(string)
return itemType == "" && hasRole

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate message contents before bypassing quarantine

When a session is tainted and remains on the same credential with previous_response_id, any single object whose type is message is classified as provider-neutral without inspecting its fields. A client carrying one replayed message with id/provider_item_id, or an input_file containing a foreign file_id, therefore bypasses normalizeProviderBoundResponseHistory and sends credential-bound state unchanged. Require the message to contain only neutral incremental fields, or run the existing bound-field/resource checks before taking this bypass.

Useful? React with 👍 / 👎.

Comment on lines +445 to +447
func providerHistorySessionIDs(headers http.Header, payload []byte, metadata map[string]any) []string {
primaryID, fallbackID := extractExplicitSessionIDs(headers, payload, metadata)
aliases := mergeSessionAliases(nil, primaryID, fallbackID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep child and parent ownership identities separate

extractExplicitSessionIDs can return a child or subagent as the primary ID and its parent as the fallback, but merging both into one ownership alias group makes independent branches overwrite each other. If two child sessions sharing a parent commit under different credentials, SessionCache.SetAliases merges the second child with the parent's existing group and rebinds the first child's primary ID; that first child is then treated as foreign and its own previous_response_id is rejected. Parent fallback IDs should not be ownership aliases for child/subagent sessions, matching the separation already used by session affinity.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant