Feature: Tool Call Router As a developer integrating multiple LLM providers I want a tool call router that normalizes provider-specific formats So that tools execute uniformly regardless of the calling provider # ---- Provider Format Detection ---- Scenario: Detect OpenAI format from arguments string Given a tool call payload with name "test/echo" and arguments '{"key": "value"}' When I detect the provider format Then the detected format should be "openai" Scenario: Detect Anthropic format from input dict Given a tool call payload with name "test/echo" and input {"key": "value"} When I detect the provider format Then the detected format should be "anthropic" Scenario: Detect LangChain format from type tool_call Given a tool call payload with name "test/echo" and type "tool_call" and args {"key": "value"} When I detect the provider format Then the detected format should be "langchain" Scenario: Detect LangChain format from args key Given a tool call payload with name "test/echo" and args key only When I detect the provider format Then the detected format should be "langchain" Scenario: Detect unknown format from empty payload Given an empty tool call payload When I detect the provider format Then the detected format should be "unknown" Scenario: Detect OpenAI format with dict arguments Given a tool call payload with name "test/echo" and dict arguments {"a": 1} When I detect the provider format Then the detected format should be "openai" # ---- Payload Normalization ---- Scenario: Normalize OpenAI payload Given a tool call payload with name "test/echo" and arguments '{"key": "value"}' When I normalize the tool call Then the normalized request tool_name should be "test/echo" And the normalized request arguments should have key "key" And the normalized request provider_format should be "openai" Scenario: Normalize Anthropic payload Given a tool call payload with name "test/echo" and input {"msg": "hello"} When I normalize the tool call Then the normalized request tool_name should be "test/echo" And the normalized request arguments should have key "msg" And the normalized request provider_format should be "anthropic" Scenario: Normalize LangChain payload Given a tool call payload with name "test/echo" and type "tool_call" and args {"data": 42} When I normalize the tool call Then the normalized request tool_name should be "test/echo" And the normalized request arguments should have key "data" And the normalized request provider_format should be "langchain" Scenario: Normalize payload with invalid JSON arguments raises error Given a tool call with invalid JSON arguments When I try to normalize the tool call Then a router ValueError should be raised containing "parse error" Scenario: Normalize payload without name raises error Given a tool call payload without a name When I try to normalize the tool call Then a router ValueError should be raised containing "name" Scenario: Normalize non-dict payload raises error When I try to normalize a non-dict payload Then a router ValueError should be raised containing "dict" Scenario: Normalize unknown format with dict arguments Given a tool call payload with name "test/echo" and parameters key {"x": 1} When I normalize the tool call Then the normalized request tool_name should be "test/echo" And the normalized request arguments should have key "x" # ---- Stable ID Generation ---- Scenario: Generate deterministic tool call ID When I generate a tool call ID for plan "plan-001" sequence 0 Then the tool call ID should start with "tc_" And the tool call ID should have length 27 Scenario: Same inputs produce same ID When I generate a tool call ID for plan "plan-001" sequence 5 And I generate another tool call ID for plan "plan-001" sequence 5 Then both tool call IDs should be identical Scenario: Different inputs produce different IDs When I generate a tool call ID for plan "plan-001" sequence 0 And I generate another tool call ID for plan "plan-001" sequence 1 Then the tool call IDs should differ Scenario: Empty plan_id raises ValueError When I try to generate a tool call ID with empty plan_id Then a router ValueError should be raised containing "plan_id" Scenario: Negative sequence raises ValueError When I try to generate a tool call ID with negative sequence Then a router ValueError should be raised containing "sequence" # ---- Router Execution ---- Scenario: Route OpenAI tool call to echo tool Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-001" And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "hi"}' When I route the tool call Then the normalized result should be successful And the normalized result tool_name should be "test/echo" And the normalized result provider_format should be "openai" And the normalized result tool_call_id should start with "tc_" Scenario: Route Anthropic tool call to echo tool Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-002" And an Anthropic-format tool call for "test/echo" with input {"msg": "hi"} When I route the tool call Then the normalized result should be successful And the normalized result provider_format should be "anthropic" Scenario: Route LangChain tool call to echo tool Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-003" And a LangChain-format tool call for "test/echo" with args {"msg": "hi"} When I route the tool call Then the normalized result should be successful And the normalized result provider_format should be "langchain" Scenario: Route tool call for missing tool Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-004" And an OpenAI-format tool call for "test/missing" with arguments '{}' When I route the tool call Then the normalized result should not be successful And the normalized result error_category should be "not_found" Scenario: Route tool call with invalid payload Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-005" And a tool call payload without a name When I route the tool call Then the normalized result should not be successful And the normalized result error_category should be "parse" Scenario: Route with provider metadata Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-006" And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "hi"}' And provider metadata with model "gpt-4" and provider_id "openai" When I route the tool call with provider metadata Then the normalized result provider_metadata should contain key "model" # ---- Batch Routing ---- Scenario: Route batch of tool calls Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-010" And a batch of 3 OpenAI-format tool calls for "test/echo" When I route the batch Then the batch should return 3 results And all batch results should be successful Scenario: Route empty batch Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-011" When I route an empty batch Then the batch should return 0 results # ---- Streaming Execution ---- Scenario: Route streaming tool call emits updates Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-020" And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "stream"}' When I route the tool call with streaming Then the stream should emit a pending update And the stream should emit a running update And the stream should emit a complete update And the stream should emit a final result Scenario: Route streaming tool call for failing tool Given a tool registry with a failing tool "test/fail" And a tool call router for plan "plan-021" And an OpenAI-format tool call for "test/fail" with arguments '{}' When I route the tool call with streaming Then the stream should emit a complete update And the stream should emit a final result that is not successful Scenario: Route streaming with invalid payload Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-022" And a tool call payload without a name When I route the tool call with streaming Then the stream should emit a final result that is not successful # ---- Validation Tool Surfacing ---- Scenario: Route call to validation tool surfaces pass result Given a tool registry with a passing validation tool "test/validator" And a tool call router for plan "plan-030" And an OpenAI-format tool call for "test/validator" with arguments '{}' When I route the tool call Then the normalized result is_validation should be True And the normalized result validation_passed should be True Scenario: Route call to validation tool surfaces fail result Given a tool registry with a failing validation tool "test/validator-fail" And a tool call router for plan "plan-031" And an OpenAI-format tool call for "test/validator-fail" with arguments '{}' When I route the tool call Then the normalized result is_validation should be True And the normalized result validation_passed should be False # ---- Error Classification ---- Scenario: Classify timeout error When I classify the error "Operation timed out after 30s" Then the error category should be "timeout" Scenario: Classify permission error When I classify the error "Permission denied: cannot write" Then the error category should be "permission" Scenario: Classify not found error When I classify the error "Tool 'x/y' not found" Then the error category should be "not_found" Scenario: Classify resource error When I classify the error "Insufficient memory for operation" Then the error category should be "resource" Scenario: Classify schema error When I classify the error "Schema validation failed" Then the error category should be "schema" Scenario: Classify parse error When I classify the error "Failed to parse response" Then the error category should be "parse" Scenario: Classify generic execution error When I classify the error "Something unexpected happened" Then the error category should be "execution" Scenario: Classify empty error When I classify an empty error message Then the error category should be "unknown" # ---- Schema Normalization ---- Scenario: Normalize schema for OpenAI provider Given a tool spec named "test/echo" with description "Echo tool" When I normalize the schema for "openai" provider Then the normalized schema should have key "parameters" And the normalized schema should have name "test/echo" Scenario: Normalize schema for Anthropic provider Given a tool spec named "test/echo" with description "Echo tool" When I normalize the schema for "anthropic" provider Then the normalized schema should have key "input_schema" Scenario: Normalize schema for LangChain provider Given a tool spec named "test/echo" with description "Echo tool" When I normalize the schema for "langchain" provider Then the normalized schema should have key "args_schema" Scenario: Normalize schema truncates long description Given a tool spec named "test/echo" with a very long description When I normalize the schema for "openai" provider with max length 50 Then the normalized schema description should be at most 50 characters Scenario: Normalize schema with invalid max length raises error Given a tool spec named "test/echo" with description "Echo tool" When I try to normalize the schema with max_description_length 0 Then a router ValueError should be raised containing "max_description_length" # ---- Schema Export ---- Scenario: Export schemas for OpenAI provider Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-040" When I export schemas for "openai" provider Then the exported schemas should contain 1 schema And the first exported schema should have tool_type "tool" # ---- Sequence Counter ---- Scenario: Router sequence increments on each route Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-050" And an OpenAI-format tool call for "test/echo" with arguments '{}' When I route the tool call Then the router sequence should be 1 When I route the tool call Then the router sequence should be 2 # ---- Router Construction Validation ---- Scenario: Router with empty plan_id raises ValueError Given a tool registry with an echo tool "test/echo" When I try to create a router with empty plan_id Then a router ValueError should be raised containing "plan_id" Scenario: Non-dict payload raises ValueError on route Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-060" When I try to route a non-dict payload Then a router ValueError should be raised containing "dict" Scenario: Non-list payloads raises ValueError on route_batch Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-061" When I try to route_batch a non-list payload Then a router ValueError should be raised containing "list" # ---- Detect format for non-dict ---- Scenario: Detect format for non-dict returns unknown When I detect format for a non-dict value Then the detected format should be "unknown" # ---- Coverage: normalize with dict args, Anthropic/LangChain branches ---- Scenario: Normalize OpenAI payload with dict arguments Given a tool call payload with name "test/echo" and dict arguments When I normalize the tool call for "openai" format Then the normalized request should have tool name "test/echo" And the normalized arguments should be a dict with key "msg" Scenario: Normalize LangChain payload with dict args Given a LangChain payload with dict args When I normalize the tool call for "langchain" format Then the normalized request should have tool name "test/echo" Scenario: Normalize LangChain payload with non-dict args raises ValueError Given a LangChain payload with list args When I try to normalize the invalid langchain args Then a router ValueError should be raised containing "must be a dict" Scenario: Normalize unknown payload with parameters key Given an unknown format payload with parameters key When I normalize the tool call for "unknown" format Then the normalized arguments should have key "query" Scenario: Normalize unknown payload with JSON string args Given an unknown format payload with JSON string in args key When I normalize the tool call for "unknown" format Then the normalized arguments should have key "data" Scenario: Normalize unknown payload with bad JSON skips to next key Given an unknown format payload with bad JSON and valid fallback When I normalize the tool call for "unknown" format Then the normalized arguments should have key "ok" # ---- Coverage: schema normalization LangChain and unknown ---- Scenario: Schema normalization for LangChain uses args_schema Given a tool spec named "test/echo" When I normalize the schema for "langchain" provider Then the schema should have key "args_schema" Scenario: Schema normalization for unknown uses input_schema Given a tool spec named "test/echo" When I normalize the schema for "unknown" provider Then the schema should have key "input_schema" # ---- Coverage: plan_id property ---- Scenario: Router plan_id property returns correct value Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-prop-test" Then the router plan_id should be "plan-prop-test" # ---- Coverage: route generic exception, validation surfacing, error classification ---- Scenario: Route catches generic RuntimeError during execution Given a tool registry with a tool that raises RuntimeError And a tool call router for plan "plan-runtime-err" When I route an OpenAI payload for the error tool Then the route result should have success false And the route result error should contain "boom" Scenario: Route returns error classification for failed ToolResult Given a tool registry with a tool that returns failed result And a tool call router for plan "plan-fail-result" When I route an OpenAI payload for the fail tool Then the route result should have success false And the route result should have error_category set Scenario: Route validates validation tool with passed result Given a tool registry with a validation tool And a tool call router for plan "plan-valid-check" When I route an OpenAI payload for the validation tool Then the route result should have is_validation true And the route result should have validation_passed true # ---- Coverage: streaming exception and validation ---- Scenario: Streaming route catches exception and yields ERROR Given a tool registry with a tool that raises RuntimeError And a tool call router for plan "plan-stream-err" When I stream-route an OpenAI payload for the error tool Then the streaming updates should include ERROR status And the streaming final result should have success false Scenario: Streaming route non-dict payload raises ValueError Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-stream-ndict" When I try to stream-route a non-dict payload Then a router ValueError should be raised containing "payload must be a dict" Scenario: Streaming route validates validation tool result Given a tool registry with a validation tool And a tool call router for plan "plan-stream-valid" When I stream-route an OpenAI payload for the validation tool Then the streaming final result should have is_validation true # ---- Coverage: get_tool_schemas validation annotation ---- Scenario: Export schemas annotates validation tools Given a tool registry with a validation tool And a tool call router for plan "plan-schema-export" When I export tool schemas for "openai" Then the exported schemas should include a tool with type "validation" Scenario: Export schemas annotates regular tools Given a tool registry with an echo tool "test/echo" And a tool call router for plan "plan-schema-regular" When I export tool schemas for "openai" Then the exported schemas should include a tool with type "tool" # ---- Coverage: _get_validation_mode ---- Scenario: Validation mode extracted from output_schema Given a tool registry with a validation tool with mode "strict" And a tool call router for plan "plan-vmode" When I export tool schemas for "openai" Then the exported validation schema should have mode "strict" Scenario: Validation mode returns None for non-string mode Given a tool registry with a validation tool with numeric mode And a tool call router for plan "plan-vmode-num" When I export tool schemas for "openai" Then the exported validation schema should not have a mode