Skip to content

feat(gateway): admit non-Anthropic models to Claude Code CLI picker#229

Open
yyyr-p wants to merge 5 commits into
Menci:mainfrom
yyyr-p:worktree-discovery-cc-picker-prefix
Open

feat(gateway): admit non-Anthropic models to Claude Code CLI picker#229
yyyr-p wants to merge 5 commits into
Menci:mainfrom
yyyr-p:worktree-discovery-cc-picker-prefix

Conversation

@yyyr-p

@yyyr-p yyyr-p commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Claude Code CLI's /model picker in CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 mode ignores every discovered id that doesn't begin with claude or anthropic, so gateway-discovered non-Anthropic models (GPT-5, Gemini, custom, …) never reach the menu even though the gateway serves them fine on /v1/messages.

  • toClaudeCodeShape prepends a claude-code: synthetic prefix on any id that doesn't already pass the CLI's /^(claude|anthropic)/i filter; display_name is preserved verbatim, and the picker renders display_name ?? id, so the operator-configured label is what the user sees.
  • enumerateModelCandidates learns a symmetric strip-retry so claude-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 UA claude-code/* and its Messages UA claude-cli/* diverge, and non-CC clients that reuse the discovered list benefit from the same symmetry.
  • Same handler additionally drops embedding / image models from the CC-shape response — the picker is chat-only, matching the .kind === 'chat' narrow that data-plane/codex/models.ts and data-plane/models/gemini.ts already apply for their own discovery surfaces.

The predicate is extracted from @anthropic-ai/[email protected]'s compiled Bootstrap Gateway /v1/models handler and cross-checked against the official protocol docs; both sources cited in the code.

Test plan

  • pnpm --filter @floway-dev/gateway run test — new serve_test.ts case pins the prefix rewrite + display_name preservation + [1m] composition + chat-only narrow (embedding + image ids absent from CC response); new registry_test.ts cases pin the strip-retry (real-id resolves, unknown prefixed id misses cleanly).
  • pnpm --filter @floway-dev/gateway run typecheck
  • pnpm run lint
  • Manual: CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 ANTHROPIC_BASE_URL=<floway>/ ANTHROPIC_AUTH_TOKEN=<key> claude/model shows non-Anthropic chat models under their real display_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).

yyyr-p added 5 commits July 16, 2026 16:42
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.
// 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:';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on hold since this PR manipulated the model resolution

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