Skip to content

Record the unsupported-tool refusal as a routing event - #31

Draft
Deep-Sixed wants to merge 1 commit into
mainfrom
claude/trusting-rubin-oe8m1j
Draft

Deep-Sixed wants to merge 1 commit into
mainfrom
claude/trusting-rubin-oe8m1j

Conversation

@Deep-Sixed

Copy link
Copy Markdown
Owner

Follow-up to #23, covering two gaps it left: the refusal emitted no routing event, and the caller-field rule wasn't documented.

Problem

#23 answers a non-function tool on a dispatch-path alias with 400 unsupported_tool before any upstream call. The check runs in app.py and covers dispatch, free and jev aliases plus direct free models. It emitted no routing event, so the refusal never reached /admin/events or the telemetry sink. Fusion and jev-router record their own caller-field refusals. docs/architecture.md also never described the dispatch-path rule. It documents jev-router's allowlist only.

Change

  • router/dispatch.py: new invalid_request_event(...), built like unauthorized_event: status 400, outcome invalid_request, no attempts, no target. invalid_request is the outcome jev-router already uses for the same situation: "the caller sent a field the alias does not accept; nothing went upstream". The admin UI already styles it.
  • app.py: the unsupported_tool refusal now emits that event with the caller's identity and the request's config version and checksum. The 400 body gains a request_id that matches the event. This is additive: message and reason are unchanged.
  • docs/architecture.md: a new paragraph in Dispatch control plane covers:
    • the body a provider receives is limited to the CALLER_FIELDS allowlist, with model replaced by the target
    • what is dropped silently (models, route, plugins, preset, provider, …) and why
    • the function-tool refusal and its routing event
    • how fusion and jev-router differ

Not changed: fusion still records its own caller-field refusal as upstream_error/400, from before invalid_request existed. The doc doesn't name fusion's outcome, so aligning it later won't require a doc change.

Tests

tests/test_caller_fields.py::test_server_tool_refusal_is_a_routing_event covers an openrouter:fusion entry in tools and an openrouter:fusion tool_choice. Each case checks:

  • the response is 400 and nothing reaches the mocked upstream
  • exactly one event is emitted, with outcome invalid_request and status 400
  • the event's alias, mode, identity and config version/checksum are correct
  • the event's request_id equals the one in the response

The test fails on current main and passes with this change. ./scripts/ci.sh (ruff + pytest) passes with 433 tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TVM2AGUsSu6Fyr5s5LSvew


Generated by Claude Code

PR #23 refuses a non-function tool on dispatch-path aliases with a 400
before any upstream call, but emitted no routing event, so the refusal
never appeared in /admin/events or the telemetry sink. Fusion and
jev-router already record their own caller-field refusals.

The refusal now emits an invalid_request event (status 400, no
attempts) under the caller's identity and the request's revision,
and the 400 body carries the event's request_id so the two can be
matched. docs/architecture.md now states the dispatch-path caller-field
rule: the CALLER_FIELDS allowlist, what is dropped and why, the
function-tool refusal, and how fusion and jev-router differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVM2AGUsSu6Fyr5s5LSvew
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