feat(discovery): fetch credential-scoped Anthropic models - #244
Conversation
Discover models per request from the first eligible Anthropic upstream, preferring the default provider and using the caller's effective credential. Follow pagination with a two-second timeout and silently fall back on every failure without touching inference pool state. Let a successful live response supersede the fallback so unavailable model IDs are not re-advertised. Expand the builtin snapshot to the 11-model public API superset with display names, timestamps, and token limits, and populate response boundary IDs. Update the README, discovery and gateway protocol documentation, and all localized model-discovery, configuration, endpoint, and Claude Desktop guides to describe the observable behavior.
There was a problem hiding this comment.
Code Review
This pull request implements live model discovery from the Anthropic upstream, enabling credential-scoped model lists with a fallback to an updated builtin snapshot. Feedback on the changes suggests optimizing URL formatting and query string allocations outside the pagination loop, and raises a concern regarding potential concurrent OAuth token refresh race conditions when bypassing the AccountPool.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…account set Review-fix pass over the credential-scoped `/v1/models` fetch. - Apply one overall `tokio::time::timeout` across credential resolution and every page. `FETCH_TIMEOUT` was attached per request inside a five-page loop, and the OAuth refresh ran before it with no timeout at all (the shared `reqwest::Client` sets no default and there is no timeout layer), so discovery could exceed its documented sub-3 s budget indefinitely. - Resolve Claude OAuth accounts through `auth::shared::resolve_pool_accounts` instead of reading `provider.accounts` directly, and skip disabled accounts. The old path ignored store-scanned accounts and `account_scope`, so the live fetch was permanently inert for those deployments and silently fell back. - Treat incomplete pagination as a failed refresh. Exhausting the page cap or receiving `has_more` without a usable cursor returned a partial list that superseded the builtin snapshot and was emitted with `has_more: false`, making a truncated catalog indistinguishable from a complete one. - Stop relaying a credential shunt's own inbound auth gate consumed. Discovery requires gateway auth whenever configured, so a client token presented in `authorization`/`x-api-key` was forwarded upstream as if it were an Anthropic credential. - Raise upstream failure logs from `debug` to `warn`. The default filter is `shunt=info`, so a persistently failing refresh was invisible while a plausible-looking snapshot was served. - Reuse `strip_duplicate_oauth_api_key` rather than reimplementing the `sk-ant-oat` bearer check verbatim. - Guard the env-mutating tests with `TEST_ENV_LOCK`; cargo runs tests concurrently, so the previous single-threaded SAFETY premise was false.
Match the documentation to the reviewed behavior of the live `/v1/models` fetch, across README, docs/, and the site in all four locales. - Scope the per-caller entitlement claim to `auth = "passthrough"`. Under `api_key` and `claude_oauth` the catalog comes from a gateway-owned credential shared by every caller, so "each caller sees its own entitled list" was only ever true for one of the three modes. - Describe `claude_oauth` as the first resolvable, non-disabled account from the same effective set inference uses, including store-scanned accounts and `account_scope` ordering. - State that a consumed `[server.auth]` or gateway-login credential is not replayed upstream, and that discovery logs why it fell back. - Document the five-page pagination cap and that any incomplete or unfollowable paginated response falls back rather than returning partial data, and add the empty-response fallback condition. - Note that upstream errors, timeouts, and malformed responses log at `warn`. - Add the optional `capabilities` field to the documented list shape. - Refresh the Anthropic provider page, which still described discovery as advertising the builtin catalog rather than live-first with builtin fallback.
Three findings verified as pre-existing or by-design rather than introduced by the credential-scoped discovery change, so later review runs skip re-auditing them: passthrough credential forwarding (the Messages path does the same), and the stale `[[models]]` endpoint-table wording in the site reference and docs/running.md (byte-identical on main since #208).
Greptile SummaryThis PR adds live, credential-scoped Anthropic model discovery with bounded pagination and snapshot fallback.
Confidence Score: 3/5The PR is not yet safe to merge because Claude OAuth discovery can advertise a catalog from a different account than inference will use. Discovery still selects the first resolvable enabled OAuth account, while inference applies pool priority, quota, cooldown, rotation, and session selection; because catalogs are credential-scoped, a multi-account deployment can expose models that the serving account cannot use or omit models that it can. Files Needing Attention: src/discovery/upstream.rs, src/adapters/anthropic/mod.rs
|
| Filename | Overview |
|---|---|
| src/discovery/upstream.rs | Implements bounded live catalog fetching and default-provider alignment, while the previously reported mismatch between first-account discovery and pooled inference selection remains. |
| src/discovery.rs | Integrates live results with curated models, snapshot fallback, expanded metadata, deduplication, and populated envelope cursors. |
| src/adapters/anthropic/mod.rs | Exposes existing OAuth-header normalization helpers for reuse by discovery without changing their behavior. |
| src/auth/inbound.rs | Exposes bearer-token parsing within the crate so discovery can avoid forwarding credentials consumed by gateway authentication. |
Sequence Diagram
sequenceDiagram
participant Client
participant Discovery as GET /v1/models
participant Auth as Credential resolution
participant Upstream as Default Anthropic provider
Client->>Discovery: Request model catalog
Discovery->>Auth: Resolve effective credential
alt Credential and Anthropic default available
Auth-->>Discovery: Upstream credential
Discovery->>Upstream: GET /v1/models (2 s overall deadline)
Upstream-->>Discovery: Credential-scoped pages
Discovery-->>Client: Curated entries + live catalog
else Resolution or upstream request fails
Discovery-->>Client: Curated entries + builtin snapshot
end
Reviews (7): Last reviewed commit: "refactor(discovery): name the consumed-b..." | Re-trigger Greptile
The new discovery coverage tripped SonarCloud's new-code duplication gate at 16.4% (limit 3%): the wiremock setup was copy-pasted across the suite, with 15 `AppState::new` blocks, 14 identical mock chains, and 10 byte-identical single-model response bodies. Extract `single_model_page`, `mount_models_ok`, `mount_models_ok_with_headers`, `mount_models_ok_after_id`, and `state_for`, and use them wherever the pattern repeated. Tests that genuinely need a bespoke mock — the paginating responder, the delayed-response deadline case, the unusable-cursor raw body, the 401, and the empty list — keep theirs. No test was renamed, removed, or weakened, and the `TEST_ENV_LOCK` guards are untouched; the suite is unchanged at 1238 passing.
…del table SonarCloud's copy-paste detector read the eleven `BuiltinModel` struct literals as a 70-line duplicated block (src/discovery.rs:36-105 against 43-112), which alone kept new-code duplication above the 3% gate. Expand the rows through a small `builtin_models!` macro so each model occupies one line. The table stays the single place an upstream catalog change is recorded, and reads more like the data it is. No behavior change: `default_returns_builtin_models_in_api_order` asserts all eleven entries with their exact ids, display names, `created_at`, and token limits, so the rewritten rows are verified rather than assumed.
The models URL and the `limit` query value were rebuilt on every pagination iteration. Format both once before the loop. Applies gemini-code-assist's review suggestion on #244.
|
@greptileai review |
|
/gemini review |
|
@greptileai review |
There was a problem hiding this comment.
Code Review
This pull request implements live model discovery for the Anthropic upstream, querying the default provider using its configured authentication mode (passthrough, API key, or OAuth) and falling back to an updated offline snapshot of 11 models if the call fails or times out. It includes extensive documentation updates across multiple languages and robust test coverage. The review feedback recommends minor optimizations and readability enhancements, specifically using a static string literal for the page limit query parameter to avoid allocations, and extracting complex boolean logic into a descriptive variable to clarify when a bearer token is considered consumed.
|



Summary
Fetch the caller's credential-scoped Anthropic model catalog on each
GET /v1/modelsrequest instead of relying only on a quickly stale builtin snapshot.A live re-probe of reference
claude gateway2.1.220 showed that its catalog had addedclaude-opus-5within roughly ten days of the previous snapshot. More importantly, three real responses confirmed that the model list varies by credential:x-api-key: 11 model IDs (the superset)claude-opus-4-1-20250805)claude-opus-4-5-20251101)Because a shared cache could expose one caller's entitlement view to another, discovery remains stateless and uncached.
This PR:
src/discovery/upstream.rs, which queries the first Anthropic-kind upstream per request, preferringserver.default_providerand then declaration order.passthrough, the configured key forapi_key, or the first resolvable account forclaude_oauth.has_morepagination, and relayscapabilitiesverbatim.AccountPoolso discovery does not affect inference selection, cooldown, or quota bookkeeping.first_id/last_idvalues.Milestone / spec
M3 —
docs/m3-discovery.md§4 and §8Also updates
docs/gateway-protocol.md,README.md, and all four locales for:guides/model-discoveryreference/configurationreference/endpointsguides/connect-claude-desktopChecklist
cargo buildpasses (covered by the successful clippy and test builds)cargo testpasses (1,230 passed, 0 failed withcargo test --all-features --workspace)cargo clippy --all-targets -- -D warningsclean (0 warnings with all features enabled)cargo fmt --all --checkcleandocs/updated if this change deviates from itREADME.md/site/as applicable (wiki/is generated; don't hand-edit)Notes for reviewers
Behavior changes
/v1/modelsmay now make one upstream request sequence. The previous "no upstream call" guarantee indocs/m3-discovery.mdis updated. Each call is capped at 2 seconds and falls back, preserving the documented sub-3-second budget.created_at,max_input_tokens,max_tokens, andcapabilitiesfields. Curated[[models]]entries retain the previous narrow shape.auto_include_builtin_models = falseremains strictly curated and now also suppresses the upstream call.Credential handling
The implementation removes a duplicate
x-api-keywhen ansk-ant-oat…bearer credential is forwarded. Please review the passthrough, configured API-key, and Claude OAuth resolution paths closely.Tests
The discovery suite adds 15 wiremock-backed tests covering:
received_requestsx-api-keybeside ansk-ant-oat…bearerapi_keyinjectionhas_morepaginationOpen question
The upstream lookup currently runs whenever an Anthropic-kind upstream is configured, with no opt-out. A slow or unreachable Anthropic upstream can therefore add up to the 2-second timeout to every
/v1/modelsrequest before fallback. If deployments need explicit control, an opt-out config key can be added as a small follow-up.Summary by cubic
Serve a live, credential‑scoped Anthropic model list on
GET /v1/modelswith a single 2 s overall timeout and a snapshot fallback. Live fetch runs only when theserver.default_provideris Anthropic‑kind; otherwise we serve the 11‑model snapshot with upstream metadata and populated first/last ids.New Features
passthrough(forwards caller token; never replays gateway auth),api_key(configured key), orclaude_oauth(first resolvable, non‑disabled account from the effective set). Stateless; noAccountPool. Relaycapabilities, followhas_morepagination (max 5 pages), and drop a duplicatedx-api-keybeside ansk-ant-oat…bearer.auto_include_builtin_models = falsereturns only curated entries and suppresses the upstream call.Bug Fixes
Written for commit 61edb47. Summary will update on new commits.
Demo