Skip to content

fix(server): refuse what a chat backend cannot serve in the gateway's words - #823

Merged
krisztian-gajdar merged 3 commits into
mainfrom
fix/messages-server-capability-checks
Oct 19, 2026
Merged

krisztian-gajdar merged 3 commits into
mainfrom
fix/messages-server-capability-checks

Conversation

@krisztian-gajdar

@krisztian-gajdar krisztian-gajdar commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

Three gaps on sie-server's POST /v1/messages (#815), all in the chat route it serves through.

  1. Tool and image refusals in the gateway's words. /v1/chat/completions on sie-server now refuses, before the engine runs and with the gateway's status, envelope and wording:

    • tools on a model whose config does not declare tasks.generate.capabilities.tools: 400 {"message": "Model '<id>' does not support tool calling", "type": "invalid_request_error", "param": "tools", "code": "unsupported_field"};
    • an image_url or input_image part the serving path cannot read: 400 {"message": "Model '<id>' does not support image input", ..., "param": "messages", "code": "unsupported_field"}.

    Tool calling is checked first, as on the gateway. /v1/messages turns these into tools: Model '<id>' does not support tool calling and <path of the first image block>: Model '<id>' does not support image input, the refusal Claude Code recovers from by replacing the image with a note. Before, a text-only model answered messages: remote chat does not support media messages on the fake or a remote upstream, and the MLX and SGLang children were left to refuse it themselves.

    The image rule is the gateway's (inputs.image and a generate task), narrowed by what this server can deliver: only the SGLang child on CUDA reads images. The MLX child reads text only, and adapter-owned chat (the fake, remote upstreams) carries no media, so an image there is refused the same way even when the model declares image input. Video keeps its existing device check.

    sie-fake now declares capabilities.tools: true, since its scripted chat makes tool calls.

  2. Prompt-too-long with token counts on the fake. The fake engine refuses a prompt that leaves no room in its window with the worker's wording, prompt_tokens (601) exceeds context_length (512) for model 'sie-fake'. So /v1/messages answers prompt is too long: 601 tokens > 512 maximum, the form Claude Code's compaction matches, instead of prompt is too long alone.

  3. The MLX backend enforces the declared window. mlx_lm.server serves any prompt length, so a 74,847-token prompt ran on the 32,768-token Qwen/Qwen3.5-4B:long-context profile. MLXGenerationAdapter.check_chat_window counts the prompt the child renders: the repo's tokenizer and chat template, with text parts joined and tool-call arguments parsed as mlx_lm.server does. A prompt that does not fit with its max_tokens is refused, as SGLang's --context-length refuses it, with the worker's wording prompt_tokens (P) + max_new_tokens (N) = T exceeds context_length (C) for model '<id>'. A prompt the tokenizer or template cannot count is left to the child.

Shared vectors

conformance/anthropic_messages/capability_vectors.json holds 7 vectors. Each has a Messages request to a model with the declared capabilities, the chat route's answer to its translation, and the Messages answer. The gateway (handlers::messages::tests::capability_vectors_are_refused_by_both_routes_before_dispatch) and the server (test_a_capability_vector_is_refused_by_both_routes_before_the_engine) replay every vector through both routes and check that nothing reached the engine. The gateway's wording was pinned only by its own unit tests before. It passes the vectors unchanged.

Validation

  • mise run lint, mise run typecheck: clean. Gateway: cargo fmt --check and clippy --all-targets -D warnings clean, handlers::messages tests 24 passed (1 new).
  • packages/sie_server/tests, offline as in CI: 14,180 passed, 605 skipped. The 6 failures are the serving-artifact materialization tests, which fail on macOS file-system semantics (renaming a read-only tree) and do not touch this change.
  • Examples: Claude Code 18 passed. OpenCode 99 passed, including conformance.py and conformance.py --api messages against a real sie-server on the fake engine.
  • conformance.py --api messages against a real sie-server on the fake: 14 pass, 1 fail (parallel_tool_use, one call per turn), 3 recorded, as before. On the same server, an image on sie-fake:chat is refused as messages.0.content.1.image: Model 'sie-fake:chat' does not support image input, and 600 words on sie-fake as prompt is too long: 601 tokens > 512 maximum, buffered and streamed.
  • Apple M3 Max, MLX backend, mise run serve -- -b sglang, Qwen/Qwen3.5-4B:long-context:
    • conformance.py --api messages: 13 pass, 0 fail, 5 recorded. prompt_above_64k_tokens is now recorded as refused with prompt is too long: 78943 tokens > 32768 maximum (74,847 prompt tokens and the 4,096-token cap). conformance.py: 9 pass, 0 fail, 3 recorded, with prompt_tokens (74847) + max_new_tokens (4096) = 78943 exceeds context_length (32768).
    • The count matches the backend's: a prompt the backend reported as 30,270 usage.prompt_tokens was refused at max_tokens 2,499 as prompt_tokens (30270) + max_new_tokens (2499) = 32769 exceeds context_length (32768), and through /v1/messages as prompt is too long: 32769 tokens > 32768 maximum. The 74,847 is also the count the backend reported when it served that prompt before this change.
    • An image in a tool result is refused as messages.2.content.0.tool_result.content.1.image: Model 'Qwen/Qwen3.5-4B:long-context' does not support image input.
    • Recorded in examples/coding-agent-claude-code/README.md.
  • Mutation checks, each killed by the new tests: no tool-calling gate; no image gate; the image gate before the tool gate; the image gate ignoring the device; the image gate ignoring adapter-owned chat; input_image parts not counted; the image refusal naming another param; the fake refusal without counts; the MLX window not checked; refusing at equality; ignoring max_tokens; counting without tools; without chat template kwargs; without parsed tool-call arguments; without joined text parts; failing closed on an uncountable prompt; no undeclared-window guard.

Not changed

  • On the SGLang backend the engine enforces the window, and the route still reports its refusal as upstream generation request failed, without counts. A remote upstream's refusal reads prompt is too long without counts. The example's limitations say so.
  • The Claude Code laptop gate was not re-run. Its recorded sessions stay far inside the window, and the README says when they were recorded.
  • No credential handling changed.

Anthropic does not support Claude Code with models other than Claude. Nothing here is supported or endorsed by Anthropic.

Summary by CodeRabbit

  • New Features
    • Requests that use unsupported tools or image inputs now receive clear, consistent errors across chat and Anthropic Messages routes, before reaching an engine.
    • Image requests can be forwarded to supported CUDA vision models.
  • Bug Fixes
    • MLX chat requests that exceed the configured context window are rejected with an input-too-long error that includes token counts and the model limit.
    • Context-window refusals now provide more consistent details across supported routes and backends.
  • Documentation
    • Updated coding-agent examples to reflect prompt-length and image-input refusals.

… words

sie-server's /v1/chat/completions, which its /v1/messages serves through, now
refuses before the engine runs, with the gateway's status, envelope and wording:
tools on a model that does not declare tool calling, and an image the serving
path cannot read. Only the SGLang child on CUDA reads images; the MLX child
reads text only and adapter-owned chat carries no media. Tool calling is
checked first, as on the gateway. The Messages route turns these into
"tools: ..." and the path of the first image block, the refusal Claude Code
recovers from. The fake model declares the tool calling its scripted chat does.

The MLX backend enforces the declared window, as SGLang's --context-length
does: the prompt is counted as mlx_lm.server renders it, with the repo's
tokenizer and chat template, and a prompt that does not fit with its
max_tokens is refused with the worker's wording. The fake engine's window
refusal carries its counts in the same wording, so both reach Messages
clients as "prompt is too long: T tokens > C maximum".

New shared vectors in conformance/anthropic_messages/capability_vectors.json
pin both chat routes' refusals and the Messages answers. The gateway's and the
server's tests replay each vector through both routes.
…X run

The Messages conformance check against Qwen/Qwen3.5-4B:long-context on Apple Silicon now records the 64K-plus prompt as refused with Claude Code's compaction wording, and the limitations name the backends whose window refusal still carries no counts.
@krisztian-gajdar
krisztian-gajdar requested a review from a team as a code owner October 19, 2026 16:06
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 5930017d-ef42-4269-86f0-4e204e99fe40
📥 Commits

Reviewing files that changed from the base of the PR and between f27d528 and 4db5ba9.

📒 Files selected for processing: 14
  • conformance/anthropic_messages/capability_vectors.json
  • examples/coding-agent-claude-code/README.md
  • examples/coding-agent-opencode/README.md
  • packages/sie_gateway/docs/architecture-guide.md
  • packages/sie_gateway/src/handlers/messages/tests.rs
  • packages/sie_server/models/sie-fake.yaml
  • packages/sie_server/src/sie_server/adapters/fake/adapter.py
  • packages/sie_server/src/sie_server/adapters/mlx/generation.py
  • packages/sie_server/src/sie_server/api/openai_local.py
  • packages/sie_server/tests/adapters/test_sglang_chat_tools.py
  • packages/sie_server/tests/api/test_anthropic_messages_route.py
  • packages/sie_server/tests/api/test_fake_chat_route.py
  • packages/sie_server/tests/api/test_openai_local.py
  • packages/sie_server/tests/api/test_remote_chat.py

Included review availability: This review used your included allowance. 6 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.

📝 Walkthrough

Chat routes now reject requests that use unsupported tools or images. MLX chat requests also receive a context-window check based on the rendered prompt and requested output tokens. Shared vectors and route tests cover these refusals.

Changes: Chat request validation

Layer / File(s) Summary
Capability refusal contract
conformance/anthropic_messages/capability_vectors.json, packages/sie_gateway/docs/architecture-guide.md
The shared vectors define tool and image refusal cases, including error details and precedence. The guide now describes coverage of both chat routes.
Capability checks before dispatch
packages/sie_server/src/sie_server/api/openai_local.py, packages/sie_server/tests/api/test_openai_local.py, packages/sie_server/tests/api/test_remote_chat.py, packages/sie_server/tests/api/test_fake_chat_route.py, packages/sie_server/models/sie-fake.yaml, packages/sie_server/tests/adapters/test_sglang_chat_tools.py
The chat route checks tool and image capabilities before dispatch. Tests cover unsupported requests, image forwarding, and remote model loading. The fake model and SGLang test configuration declare tool support.
Cross-route refusal conformance
packages/sie_gateway/src/handlers/messages/tests.rs, packages/sie_server/tests/api/test_anthropic_messages_route.py
Gateway and server tests run the refusal vectors through chat and Messages routes, check response details, and confirm that requests are not dispatched.
MLX context-window checks
packages/sie_server/src/sie_server/adapters/mlx/generation.py, packages/sie_server/src/sie_server/api/openai_local.py, packages/sie_server/src/sie_server/adapters/fake/adapter.py, packages/sie_server/tests/api/test_openai_local.py, packages/sie_server/tests/api/test_fake_chat_route.py, packages/sie_server/tests/api/test_anthropic_messages_route.py, examples/coding-agent-claude-code/README.md, examples/coding-agent-opencode/README.md
MLX counts rendered prompt tokens and rejects requests that exceed the configured limit. Tests check error details, fitting requests, and cases where prompt counting is skipped. Example reports describe the recorded refusals and backend-specific behavior.

Suggested reviewers: svonava, huronat

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 4db5b

The capability refusals and MLX window check have no identified merge-blocking defect. The change is ready to merge after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 19.61% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 51 functions across 9 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title accurately summarizes the main change: the server now refuses unsupported chat capabilities using gateway-aligned errors and wording.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 19.61% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 51 functions across 9 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@krisztian-gajdar
krisztian-gajdar merged commit fb06db0 into main Oct 19, 2026
45 checks passed
@krisztian-gajdar
krisztian-gajdar deleted the fix/messages-server-capability-checks branch October 19, 2026 17:00
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.

1 participant