diff --git a/tests/data/wire_payloads/anthropic_hoist__native_orphan.json b/tests/data/wire_payloads/anthropic_hoist__native_orphan.json new file mode 100644 index 00000000..849d51ea --- /dev/null +++ b/tests/data/wire_payloads/anthropic_hoist__native_orphan.json @@ -0,0 +1,70 @@ +{ + "cache_control": { + "type": "ephemeral" + }, + "max_tokens": 4096, + "messages": [ + { + "content": "Think about the weather.", + "role": "user" + }, + { + "content": [ + { + "signature": "sig-abc", + "thinking": "The user wants weather.", + "type": "thinking" + }, + { + "text": "Let me check.", + "type": "text" + }, + { + "id": "call_1", + "input": { + "city": "Paris" + }, + "name": "get_weather", + "type": "tool_use" + } + ], + "role": "assistant" + }, + { + "content": [ + { + "content": "Tool execution was cancelled.", + "is_error": true, + "tool_use_id": "call_1", + "type": "tool_result" + } + ], + "role": "user" + } + ], + "model": "claude-sonnet-4-6", + "output_config": { + "effort": "medium" + }, + "temperature": 1.0, + "thinking": { + "type": "adaptive" + }, + "tools": [ + { + "description": "Look up the weather for a city.", + "input_schema": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "name": "get_weather" + } + ] +} diff --git a/tests/data/wire_payloads/anthropic_native__native_orphan.json b/tests/data/wire_payloads/anthropic_native__native_orphan.json new file mode 100644 index 00000000..bd675879 --- /dev/null +++ b/tests/data/wire_payloads/anthropic_native__native_orphan.json @@ -0,0 +1,70 @@ +{ + "cache_control": { + "type": "ephemeral" + }, + "max_tokens": 4096, + "messages": [ + { + "content": "Think about the weather.", + "role": "user" + }, + { + "content": [ + { + "signature": "sig-abc", + "thinking": "The user wants weather.", + "type": "thinking" + }, + { + "text": "Let me check.", + "type": "text" + }, + { + "id": "call_1", + "input": { + "city": "Paris" + }, + "name": "get_weather", + "type": "tool_use" + } + ], + "role": "assistant" + }, + { + "content": [ + { + "content": "Tool execution was cancelled.", + "is_error": true, + "tool_use_id": "call_1", + "type": "tool_result" + } + ], + "role": "user" + } + ], + "model": "claude-opus-4-8", + "output_config": { + "effort": "medium" + }, + "thinking": { + "display": "summarized", + "type": "adaptive" + }, + "tools": [ + { + "description": "Look up the weather for a city.", + "input_schema": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "name": "get_weather" + } + ] +} diff --git a/tests/data/wire_payloads/google__native_orphan.json b/tests/data/wire_payloads/google__native_orphan.json new file mode 100644 index 00000000..a3729c19 --- /dev/null +++ b/tests/data/wire_payloads/google__native_orphan.json @@ -0,0 +1,54 @@ +{ + "max_tokens": 4096, + "messages": [ + { + "content": "Think about the weather.", + "role": "user" + }, + { + "content": "Let me check.", + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"city\": \"Paris\"}", + "name": "get_weather" + }, + "id": "call_1", + "type": "function" + } + ] + }, + { + "content": "Tool execution was cancelled.", + "role": "tool", + "tool_call_id": "call_1" + } + ], + "model": "gemini-2.5-pro", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.5, + "tools": [ + { + "function": { + "description": "Look up the weather for a city.", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + }, + "type": "function" + } + ] +} diff --git a/tests/data/wire_payloads/openai_chat__native_orphan.json b/tests/data/wire_payloads/openai_chat__native_orphan.json new file mode 100644 index 00000000..ece1e77b --- /dev/null +++ b/tests/data/wire_payloads/openai_chat__native_orphan.json @@ -0,0 +1,54 @@ +{ + "max_completion_tokens": 4096, + "messages": [ + { + "content": "Think about the weather.", + "role": "user" + }, + { + "content": "Let me check.", + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"city\": \"Paris\"}", + "name": "get_weather" + }, + "id": "call_1", + "type": "function" + } + ] + }, + { + "content": "Tool execution was cancelled.", + "role": "tool", + "tool_call_id": "call_1" + } + ], + "model": "gpt-4o-mini", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.5, + "tools": [ + { + "function": { + "description": "Look up the weather for a city.", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + } + }, + "type": "function" + } + ] +} diff --git a/tests/data/wire_payloads/openai_responses__native_orphan.json b/tests/data/wire_payloads/openai_responses__native_orphan.json new file mode 100644 index 00000000..8b73d35d --- /dev/null +++ b/tests/data/wire_payloads/openai_responses__native_orphan.json @@ -0,0 +1,55 @@ +{ + "include": [ + "reasoning.encrypted_content" + ], + "input": [ + { + "content": "Think about the weather.", + "role": "user", + "type": "message" + }, + { + "content": "Let me check.", + "role": "assistant", + "type": "message" + }, + { + "arguments": "{\"city\": \"Paris\"}", + "call_id": "call_1", + "name": "get_weather", + "type": "function_call" + }, + { + "call_id": "call_1", + "output": "Tool execution was cancelled.", + "type": "function_call_output" + } + ], + "max_output_tokens": 4096, + "model": "gpt-5", + "prompt_cache_retention": "24h", + "reasoning": { + "effort": "medium" + }, + "store": false, + "stream": true, + "tools": [ + { + "description": "Look up the weather for a city.", + "name": "get_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "type": "object" + }, + "strict": false, + "type": "function" + } + ] +} diff --git a/tests/test_wire_payload_golden.py b/tests/test_wire_payload_golden.py index 793c8815..3a6e53b7 100644 --- a/tests/test_wire_payload_golden.py +++ b/tests/test_wire_payload_golden.py @@ -211,6 +211,25 @@ FIX_NATIVE_REASONING: list[dict[str, Any]] = [ {"role": "tool", "tool_call_id": "call_1", "content": "18C, clear."}, ] +# Native reasoning lane with an UNANSWERED tool_use — the verbatim-replay orphan. +# The native ``tool_use`` is mirrored top-level in ``tool_calls`` (the P1 invariant), +# so the send-time repair can synthesize its cancellation result by reading +# ``tool_calls`` alone. Exercises the Anthropic verbatim-replay branch that the old +# per-provider ``pc_tool_ids`` synthesis covered. +FIX_NATIVE_ORPHAN: list[dict[str, Any]] = [ + {"role": "user", "content": "Think about the weather."}, + { + "role": "assistant", + "content": "Let me check.", + "_provider_content": [ + {"type": "thinking", "thinking": "The user wants weather.", "signature": "sig-abc"}, + {"type": "text", "text": "Let me check."}, + {"type": "tool_use", "id": "call_1", "name": "get_weather", "input": {"city": "Paris"}}, + ], + "tool_calls": [_tc("call_1")], + }, +] + # Multipart user content (image attachment as the provider receives it today). FIX_MULTIPART: list[dict[str, Any]] = [ { @@ -249,6 +268,10 @@ _FIXTURES: dict[str, tuple[list[dict[str, Any]], dict[str, Any]]] = { FIX_NATIVE_REASONING, {"tools": _TOOLS, "replay_reasoning_to_model": True}, ), + "native_orphan": ( + FIX_NATIVE_ORPHAN, + {"tools": _TOOLS, "replay_reasoning_to_model": True}, + ), "multipart": (FIX_MULTIPART, {}), "operator_system": (FIX_OPERATOR_SYSTEM, {"tools": _TOOLS}), }