Bug summary
Switching providers or models in an existing conversation can fail when the session contains tool calls created by another provider. Tau currently persists a provider-produced tool-call identifier directly in the provider-neutral ToolCall.id field and reuses that value when serializing history for the next provider.
This is incompatible with providers that use different identifier formats or semantics.
A concrete failure occurs when switching from the OpenAI Codex Responses provider to Anthropic. A Codex function call contains two identifiers:
{
"type": "function_call",
"id": "fc_0ca1b059695ff22f...",
"call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI"
}
Tau currently combines these values into one persisted ID in src/tau_ai/openai_codex.py:
call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...
The Codex adapter can split this value when replaying the conversation to Codex, but the Anthropic adapter forwards it unchanged as both tool_use.id and tool_result.tool_use_id. Anthropic requires tool-use IDs to match:
The | separator is invalid, so Anthropic rejects the entire request with HTTP 400:
messages.1.content.0.tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$'
This reveals a broader abstraction leak: the single ToolCall.id field currently acts as Tau's correlation ID, a provider-native tool-call ID, and—in the Codex case—an encoding of multiple provider-native IDs.
How to reproduce it
- Start a Tau session using the
openai-codex provider.
- Ask the model to perform an operation that invokes at least one tool.
- Allow the tool call and corresponding tool result to be persisted in the session.
- Switch the active model/provider to Anthropic without starting a new session.
- Send another user message.
- Observe that Anthropic rejects the compiled history before generating a response.
The outgoing Anthropic payload contains a block resembling:
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...",
"name": "bash",
"input": {}
}
]
}
and a corresponding result resembling:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...",
"content": "..."
}
]
}
Relevant serialization paths include:
src/tau_agent/messages.py: provider-neutral ToolCall and ToolResultMessage models
src/tau_ai/openai_codex.py: combines and later splits Codex call_id and item id
src/tau_ai/anthropic.py: forwards persisted IDs into Anthropic tool_use and tool_result blocks
src/tau_ai/openai_compatible.py: forwards persisted IDs into Chat Completions tool calls/results
src/tau_agent/session/jsonl.py: session compatibility and persistence
A regression test should build the history in memory; it should not require live provider credentials.
Proposed direction
Separate Tau's stable, provider-neutral tool-call identity from provider-native replay metadata, and add a target-provider history compilation step.
1. Introduce a canonical Tau correlation ID
Each tool call should have a stable Tau-owned ID such as:
tc_7c38b819be06457e9280fb16db87ee23
The canonical ID should:
- use a conservative character set accepted by all supported providers;
- remain stable in persisted sessions;
- be referenced by
ToolResultMessage.tool_call_id;
- never encode provider-specific structure.
2. Preserve provider-native metadata separately
For same-provider replay, retain the native values needed by the originating transport. For Codex Responses, that includes at least:
{
"provider": "openai-codex",
"api": "responses",
"call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI",
"item_id": "fc_0ca1b059695ff22f..."
}
The exact schema should remain typed and portable. tau_agent should define correlation semantics without learning Anthropic validation rules or Codex payload details; provider adapters in tau_ai should own native serialization.
3. Map IDs while compiling history for the target provider
When the target differs from the origin provider, generate a deterministic target-safe identifier and use it consistently for both the call and its result. A hash/base64url-derived mapping is preferable to replacing invalid characters because replacement can collide—for example, both a|b and a_b could become a_b.
Suggested behavior:
- Codex → Codex: preserve native
call_id and item id when available.
- Anthropic → Anthropic: preserve the native
tool_use.id when available.
- Codex → Anthropic: derive an Anthropic-safe ID and apply it to the call and result.
- Anthropic → Codex: derive a valid Codex correlation ID and omit optional native item metadata when it cannot be reconstructed safely.
- Any provider → OpenAI-compatible: emit a target-valid ID and use it consistently.
Parallel calls must remain distinct, and mapping must be scoped consistently across the complete outgoing history.
4. Support existing sessions
Existing JSONL sessions may contain the current call_id|item_id representation. Loading or compiling these sessions should recognize that legacy shape, derive a deterministic canonical ID, retain the parsed Codex metadata, and rewrite matching tool-result references in memory. The migration must not mutate or corrupt the original session unexpectedly.
A smaller compatibility patch could first normalize IDs only in the Anthropic adapter, but this should be treated as an interim fix. It would address the immediate HTTP 400 while leaving provider identity embedded in the core message model.
5. Implement and verify in focused stages
A PR can be developed in these stages:
- Add failing provider-conversion tests for Codex history sent to Anthropic.
- Define canonical identity and provider replay metadata semantics.
- Add deterministic target-provider ID mapping.
- Add legacy-session decoding/migration.
- Cover same-provider replay and round-trip persistence.
- Update provider/model-switching and session documentation.
Tests should cover:
- a Codex composite ID compiled for Anthropic;
- matching remapping of
tool_use.id and tool_result.tool_use_id;
- parallel calls and collision resistance;
- Anthropic history compiled for Codex and OpenAI-compatible providers;
- same-provider replay preserving native metadata;
- legacy JSONL sessions containing
call_id|item_id;
- switching providers more than once in one session;
- malformed or missing provider metadata with a safe fallback.
Expected benefit
- Users can switch models/providers mid-conversation after tools have run.
- Provider-specific identifiers no longer leak into Tau's portable agent/session abstraction.
- Same-provider replay can retain native fidelity while cross-provider replay remains valid.
- Persisted sessions become more robust as additional providers and transports are added.
- Provider conversion failures can be detected and tested without live API calls.
- Error handling can report a clear compatibility problem instead of appearing as an unresponsive agent after a provider-side HTTP 400.
Tradeoff (if any)
- The message/session schema becomes more explicit and may need a backward-compatible migration path.
- Provider adapters must maintain an ID map while compiling historical messages.
- Deterministic remapping adds modest implementation complexity, especially for parallel calls and malformed legacy history.
- Some provider-native replay data may be impossible or unsafe to translate. Cross-provider replay may need to discard optional metadata while preserving visible text, tool calls, and results.
- Existing extensions that assume
ToolCall.id is the raw provider ID may require compatibility handling and documentation.
Related follow-up
Tool-call IDs are one part of cross-provider model switching. A separate follow-up should define a complete target-provider history policy for other opaque or provider-specific state, including:
- thinking/reasoning signatures and encrypted reasoning items;
- text signatures, response IDs, and cache/replay metadata;
- differences in accepted message ordering and content-block formats;
- target-model context-window estimation and compaction before sending;
- clear TUI rendering of provider conversion and context-limit errors.
These concerns should not block the tool-ID fix, but the design should leave room for a reusable history compiler rather than accumulating unrelated conditionals in each provider adapter.
Bug summary
Switching providers or models in an existing conversation can fail when the session contains tool calls created by another provider. Tau currently persists a provider-produced tool-call identifier directly in the provider-neutral
ToolCall.idfield and reuses that value when serializing history for the next provider.This is incompatible with providers that use different identifier formats or semantics.
A concrete failure occurs when switching from the OpenAI Codex Responses provider to Anthropic. A Codex function call contains two identifiers:
{ "type": "function_call", "id": "fc_0ca1b059695ff22f...", "call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI" }Tau currently combines these values into one persisted ID in
src/tau_ai/openai_codex.py:The Codex adapter can split this value when replaying the conversation to Codex, but the Anthropic adapter forwards it unchanged as both
tool_use.idandtool_result.tool_use_id. Anthropic requires tool-use IDs to match:The
|separator is invalid, so Anthropic rejects the entire request with HTTP 400:This reveals a broader abstraction leak: the single
ToolCall.idfield currently acts as Tau's correlation ID, a provider-native tool-call ID, and—in the Codex case—an encoding of multiple provider-native IDs.How to reproduce it
openai-codexprovider.The outgoing Anthropic payload contains a block resembling:
{ "role": "assistant", "content": [ { "type": "tool_use", "id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...", "name": "bash", "input": {} } ] }and a corresponding result resembling:
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI|fc_0ca1b059695ff22f...", "content": "..." } ] }Relevant serialization paths include:
src/tau_agent/messages.py: provider-neutralToolCallandToolResultMessagemodelssrc/tau_ai/openai_codex.py: combines and later splits Codexcall_idand itemidsrc/tau_ai/anthropic.py: forwards persisted IDs into Anthropictool_useandtool_resultblockssrc/tau_ai/openai_compatible.py: forwards persisted IDs into Chat Completions tool calls/resultssrc/tau_agent/session/jsonl.py: session compatibility and persistenceA regression test should build the history in memory; it should not require live provider credentials.
Proposed direction
Separate Tau's stable, provider-neutral tool-call identity from provider-native replay metadata, and add a target-provider history compilation step.
1. Introduce a canonical Tau correlation ID
Each tool call should have a stable Tau-owned ID such as:
The canonical ID should:
ToolResultMessage.tool_call_id;2. Preserve provider-native metadata separately
For same-provider replay, retain the native values needed by the originating transport. For Codex Responses, that includes at least:
{ "provider": "openai-codex", "api": "responses", "call_id": "call_XwFnGCtoQNIN2ID9ahtpLvsI", "item_id": "fc_0ca1b059695ff22f..." }The exact schema should remain typed and portable.
tau_agentshould define correlation semantics without learning Anthropic validation rules or Codex payload details; provider adapters intau_aishould own native serialization.3. Map IDs while compiling history for the target provider
When the target differs from the origin provider, generate a deterministic target-safe identifier and use it consistently for both the call and its result. A hash/base64url-derived mapping is preferable to replacing invalid characters because replacement can collide—for example, both
a|banda_bcould becomea_b.Suggested behavior:
call_idand itemidwhen available.tool_use.idwhen available.Parallel calls must remain distinct, and mapping must be scoped consistently across the complete outgoing history.
4. Support existing sessions
Existing JSONL sessions may contain the current
call_id|item_idrepresentation. Loading or compiling these sessions should recognize that legacy shape, derive a deterministic canonical ID, retain the parsed Codex metadata, and rewrite matching tool-result references in memory. The migration must not mutate or corrupt the original session unexpectedly.A smaller compatibility patch could first normalize IDs only in the Anthropic adapter, but this should be treated as an interim fix. It would address the immediate HTTP 400 while leaving provider identity embedded in the core message model.
5. Implement and verify in focused stages
A PR can be developed in these stages:
Tests should cover:
tool_use.idandtool_result.tool_use_id;call_id|item_id;Expected benefit
Tradeoff (if any)
ToolCall.idis the raw provider ID may require compatibility handling and documentation.Related follow-up
Tool-call IDs are one part of cross-provider model switching. A separate follow-up should define a complete target-provider history policy for other opaque or provider-specific state, including:
These concerns should not block the tool-ID fix, but the design should leave room for a reusable history compiler rather than accumulating unrelated conditionals in each provider adapter.