feat(gateway): meet what Claude Desktop's third-party mode asks of a gateway - #203
Merged
Merged
Conversation
…gateway Claude Desktop in third-party inference mode, and the Claude Code engine it embeds, expect four things from a gateway that the gateway either did not do or did only partly: - A `HEAD /api/hello` connection-warming probe at startup. It fell into the passthrough and was refused with 401 for carrying no key. It is now answered with 200 by the gateway itself, without a key and without reaching an upstream. - Keep-alive pings. The client counts every byte and gives up on a stream that is silent for about five minutes. An Anthropic upstream sends its own pings while it thinks; a converted upstream sends nothing, so long reasoning looked like a dead connection. When the client speaks Anthropic Messages and the upstream has produced nothing for 15 seconds, the gateway now writes `event: ping`. Pings go only between frames (never into one the upstream left half sent), bypass the body record, the usage sniffing and the output meter, and are never sent to other formats, which have no such event. - `/v1/models` in the Anthropic shape. Anthropic clients got the OpenAI list. They now get `type`, `display_name`, `created_at`, `has_more`, `first_id` and `last_id`, and Claude models get `anthropic_family_tier`, which Desktop uses to resolve bare aliases such as `sonnet`. The OpenAI fields stay on the Anthropic objects, because clients that only send `x-api-key` are also read as Anthropic. Names are derived only from ids that read as Claude; any other id is its own display name and gets no tier. - `?beta=true`, `anthropic-beta` and `count_tokens` already worked; tests now pin them, including `cache_control` reaching the upstream untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNaZJNf7LaEo1ceW5M9tv2
This was referenced Sep 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by F · project thread
What this changes
Before: Claude Desktop's
HEAD /api/hellostartup probe got a 401, a converted upstream that went quiet left an Anthropic stream silent until the client gave up, and Anthropic clients got the OpenAI-shaped/v1/modelslist.After: the gateway answers
/api/helloitself, writesevent: pinginto Anthropic Messages streams after 15 s of upstream silence, and lists models in the Anthropic shape withdisplay_nameand, for Claude ids,anthropic_family_tier.Why
ThinkWatch Lite is adding one-click adoption of Claude Desktop (third-party inference mode). Desktop and the Claude Code engine it embeds expect, per the Desktop gateway guide and the gateway protocol guide:
HEAD /api/hellowarm-up probeGET/HEAD→ 200, no auth, never forwardedevent: pingevery 15 s of upstream silence when the client speaks Anthropic MessagesGET /v1/modelsobject,created) for every clienttype,display_name,created_at,has_more,first_id,last_id; Claude ids also getanthropic_family_tierPOST /v1/messages?beta=true,anthropic-beta/anthropic-versionverbatim,cache_controlkept/v1/messages/count_tokens?beta=truePings
pingevent, so they get nothing.tw_gateway::PING_EVERY(15 s), copied intoAppState::ping_everyso tests can shorten it.Models
objectandcreatedstay, because clients that only sendx-api-keyare also read as Anthropic.display_nameis derived only from ids whose last/segment isclaude-…made of plain alphanumeric parts (claude-sonnet-4-5-20250929→Claude Sonnet 4.5). Any other id is its own display name, which the client treats as "no name given".anthropic_family_tieris set only when the id containsclaudeoranthropicand exactly one ofopus/sonnet/haiku. Nothing marks a non-Claude model as Claude, and no model is added to the list.supports_1mandis_family_defaultare not set: both are claims about the deployment that the gateway cannot verify.How it was verified
crates/tw-gateway/tests/claude_desktop.rswith 8 tests: the probe is answered with 200 for HEAD and GET and never reaches the upstream;/v1/messages?beta=truereaches the upstream with the same path, query,anthropic-beta,anthropic-versionandcache_control(includingttl);count_tokens?beta=trueis forwarded; the Anthropic listing and the single-model lookup return the same object; OpenAI clients still get the OpenAI list; a converted OpenAI-chat upstream that stalls 700 ms produces pings, and the stream minus the pings is a complete Anthropic stream, usage is 5/2 as reported, and the recorded body has no ping; an Anthropic upstream that stalls mid-frame gets a ping only at the frame boundary, and the stream minus the pings is byte-identical to the upstream's; a Chat Completions client gets no pings.server/listing.rs.cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warningsandcargo test --workspaceall passed../scripts/smoke.sh: 60 passed, 0 failed./api/helloreturns before identification and reads nothing from the request, and the ping path writes a constant frame.Notes for review
msg!codes, no config change.tw-dialect,tw-guardandtw-breakerare untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01UNaZJNf7LaEo1ceW5M9tv2
Generated by Claude Code