Skip to content

fix(openai): replay reasoning signatures on function calls - #13

Merged
arjunkmrm merged 2 commits into
clavia-labs:clavia/ai-providersfrom
justrach:fix/openai-replay-reasoning-signatures
Oct 15, 2026
Merged

arjunkmrm merged 2 commits into
clavia-labs:clavia/ai-providersfrom
justrach:fix/openai-replay-reasoning-signatures

Conversation

@justrach

Copy link
Copy Markdown

Some providers that serve reasoning models over the Responses protocol return the model's opaque reasoning state as encrypted_content on function_call output items, not only on reasoning items. A stream can send this value only on response.output_item.done, after response.function_call_arguments.done. In a stateless (store: false) tool loop, the client must send the value back unchanged on the next request. Before this change it was lost in four places:

  • The item schema removed the field during decoding.
  • Streamed calls were emitted before the completed item arrived.
  • The tool-call part metadata did not keep the value.
  • The replayed function_call input item did not include it.

Without the value, the model can lose its reasoning state between turns, and a later turn can complete with an empty output.

Changes:

  • OpenAiSchema: the function_call item accepts an optional encrypted_content (string or null). One schema covers request input and response output, so the field survives response decoding, stream event decoding and request encoding.
  • OpenAiLanguageModel: generated responses keep the value as metadata.openai.encryptedContent on the tool-call part. Reasoning parts already use this metadata key.
  • OpenAiLanguageModel: streaming records the arguments when response.function_call_arguments.done arrives. It emits the call when response.output_item.done arrives, with that item's encrypted_content. The value from response.output_item.added is the fallback.
  • OpenAiLanguageModel: if a call's item never completes, the call is emitted at response.completed, response.incomplete or response.failed. These calls are emitted in output order, before the finish part. Without this, a stream that never completes the item would lose calls that the old argument-completion path emitted.
  • OpenAiLanguageModel: prepareMessages sends encryptedContent from tool-call part options as encrypted_content on the function_call input item. Reasoning items keep their existing replay and stay in the received order relative to function calls.
  • ToolCallPartOptions / ToolCallPartMetadata: declare encryptedContent?: string | null. The OpenAI-compatible package declares the same field. Both packages augment the same Effect module interfaces, so their declarations must stay identical.
  • CLAVIA_PATCHES.md: add a "Function call signatures" row and section.

An explicit include: ["reasoning.encrypted_content"] already reaches the request for model ids that are not in the built-in reasoning-model list. The new tests check this together with the replay. OpenAI requests are unchanged:

  • Function calls without encrypted_content replay without the key.
  • Inferred include values are unchanged.
  • In a normal stream, the call is emitted when the next event (item completion) arrives instead of on argument completion. The emitted parts and their order are unchanged.

Verification:

  • pnpm test --run packages/ai/clavia/test/LanguageModel.test.ts packages/ai/clavia/test/ProviderStreams.test.ts packages/ai/openai/test/OpenAiLanguageModel.test.ts packages/ai/anthropic/test/AnthropicLanguageModel.test.ts packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts: 5 files, 202 tests passed.
  • pnpm test --run packages/ai/openai/test packages/ai/openai-compat/test packages/ai/clavia/test: 10 files, 255 tests passed.
  • New tests:
    • Non-streamed response for a model id outside the reasoning-model list, with function call and reasoning signatures. The test checks the explicit include, the tool-call metadata, and that the replayed reasoning and function_call items carry their encrypted content in order.
    • Unchanged OpenAI requests for gpt-4o-mini and gpt-5 with store: false.
    • Streamed SSE through the real client, with interleaved reasoning and function call items. One call's signature is only on the completed item, which arrives after argument completion. After a JSON round trip of the prompt, all items replay in received order.
    • A stream with no item completion: the calls are emitted at the terminal event, in order, with the added item's signature, before a tool-calls finish.
    • The existing tests still pass, including the argument-completion-only test and the duplicate-call fixture.
  • The streamed signature test fails on the previous emission path.
  • pnpm lint-fix, pnpm lint and pnpm check pass.
  • node scripts/clavia-release.mjs pack and check pass. The packed consumer runs with skipLibCheck: false (6 files, 101 tests) and resolves a single effect@4.0.0-rc.115.
  • pnpm jsdocs --check still reports three existing issues, all in files this PR does not touch (packages/ai/clavia/src/ToolResult.ts and packages/effect/src/unstable/ai/McpSchema.ts).

Release note: publish @tardie/ai-openai and @tardie/ai-openai-compat together, because their module augmentations must match.

🤖 Generated with Claude Code

Some Responses-compatible providers return the model's opaque reasoning
state as `encrypted_content` on `function_call` items. In stateless
(`store: false`) tool loops the client must send it back unchanged,
otherwise the model loses its reasoning state between turns.

- Accept `encrypted_content` on function call items in the request and
  response schemas.
- Keep it in tool-call part metadata for generated responses and for
  streamed calls emitted on argument or item completion.
- Replay it on the `function_call` input item.
- Declare the matching tool-call metadata field in the compatible
  provider so the shared module augmentation stays identical.
A stream can send a function call's `encrypted_content` only on
`response.output_item.done`, after `response.function_call_arguments.done`.
Emitting the call on argument completion dropped the signature.

- Record arguments on `response.function_call_arguments.done` and emit
  the call on `response.output_item.done` with the completed item's
  `encrypted_content`.
- Emit calls whose item never completes at `response.completed`,
  `response.incomplete` or `response.failed`, in output order, before
  the finish part.
- Cover a signature only on the completed item and a stream without
  item completion.
@chatgpt-codex-connector

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 ⚠️ Failed 2026-10-15T05:38:14.084957Z 2f6d65a PR opened
ℹ️ 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

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@arjunkmrm
arjunkmrm force-pushed the fix/openai-replay-reasoning-signatures branch from 2f6d65a to 29fb4e7 Compare October 15, 2026 08:40
@arjunkmrm
arjunkmrm merged commit bb7b490 into clavia-labs:clavia/ai-providers Oct 15, 2026
1 check passed
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.

2 participants