feat(gateway): admit non-Anthropic models to Claude Code CLI picker#229
Open
yyyr-p wants to merge 5 commits into
Open
feat(gateway): admit non-Anthropic models to Claude Code CLI picker#229yyyr-p wants to merge 5 commits into
yyyr-p wants to merge 5 commits into
Conversation
The CLI's gateway-discovery `/model` picker filters incoming ids by `/^(claude|anthropic)/i`, silently dropping every other model. Prepend a `claude-code:` synthetic prefix in `toClaudeCodeShape` on any id that doesn't already pass the filter, and strip it back off in `enumerateModelCandidates` when the same id comes in on any data-plane endpoint. `display_name` stays untouched so the picker keeps rendering the operator's original label. Predicate extracted verbatim from `@anthropic-ai/[email protected]`'s Bootstrap Gateway handler; the `[1m]` suffix trick relies on the same de-collision from the CLI's built-in family map.
Adds a serve_test case that confirms `toClaudeCodeShape` prefixes non-Anthropic ids for the CC picker while leaving `display_name` intact and composing cleanly with the `[1m]` suffix, and two registry_test cases that pin down the strip-retry semantics: `claude-code:<real-id>` routes to the real model, and a prefixed id whose stripped form is also unknown still misses cleanly.
`toClaudeCodeShape` returned the full catalog (chat + embedding + image) so gpt-image-2, text-embedding-*, and friends showed up in the picker menu where the CLI can't dispatch to them anyway. Filter to chat-only at the top of the projection, matching the same `model.kind === 'chat'` narrow that ../codex/models.ts and ./gemini.ts already apply for their own discovery surfaces.
Anthropic documents the picker's `/^(claude|anthropic)/i` filter and the built-in-collision skip (with the Fable carve-out) at code.claude.com/docs/en/llm-gateway-protocol#model-discovery. Cite the URL as the primary source-of-truth in both places that assert the filter shape — `claude-code-prefix.ts` (predicate definition) and `serve.ts` item (2) (the load-bearing claim that motivates the synthetic prefix rewrite). The binary extraction from @anthropic-ai/[email protected] stays as corroborating detail that pins the exact carve-out family and evaluation order the prose leaves implicit.
Menci
reviewed
Jul 16, 2026
| // error the user sees; the `code:` segment plus the colon separator | ||
| // make it visually obvious to anyone reading the wire that the id is | ||
| // a gateway alias, not an upstream-native Anthropic name. | ||
| export const CLAUDE_CODE_SYNTHETIC_PREFIX = 'claude-code:'; |
Owner
There was a problem hiding this comment.
on hold since this PR manipulated the model resolution
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.
Summary
Claude Code CLI's
/modelpicker inCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1mode ignores every discovered id that doesn't begin withclaudeoranthropic, so gateway-discovered non-Anthropic models (GPT-5, Gemini, custom, …) never reach the menu even though the gateway serves them fine on/v1/messages.toClaudeCodeShapeprepends aclaude-code:synthetic prefix on any id that doesn't already pass the CLI's/^(claude|anthropic)/ifilter;display_nameis preserved verbatim, and the picker rendersdisplay_name ?? id, so the operator-configured label is what the user sees.enumerateModelCandidateslearns a symmetric strip-retry soclaude-code:<real-id>coming back in on/v1/messages(or any other data-plane endpoint) resolves to<real-id>. Unconditional (no UA gate): the CLI's discovery UAclaude-code/*and its Messages UAclaude-cli/*diverge, and non-CC clients that reuse the discovered list benefit from the same symmetry..kind === 'chat'narrow thatdata-plane/codex/models.tsanddata-plane/models/gemini.tsalready apply for their own discovery surfaces.The predicate is extracted from
@anthropic-ai/[email protected]'s compiledBootstrap Gateway /v1/modelshandler and cross-checked against the official protocol docs; both sources cited in the code.Test plan
pnpm --filter @floway-dev/gateway run test— newserve_test.tscase pins the prefix rewrite +display_namepreservation +[1m]composition + chat-only narrow (embedding + image ids absent from CC response); newregistry_test.tscases pin the strip-retry (real-id resolves, unknown prefixed id misses cleanly).pnpm --filter @floway-dev/gateway run typecheckpnpm run lintCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 ANTHROPIC_BASE_URL=<floway>/ ANTHROPIC_AUTH_TOKEN=<key> claude—/modelshows non-Anthropic chat models under their realdisplay_name; embedding/image models are not listed; picking a non-Anthropic model and sending a prompt returns a response (proves inbound strip end-to-end).