Skip to content

fix(models): list env-credentialed providers in /v1/models - #388

Merged
njbrake merged 5 commits into
mainfrom
fix/models-discovery-disagreement
Jul 23, 2026
Merged

fix(models): list env-credentialed providers in /v1/models#388
njbrake merged 5 commits into
mainfrom
fix/models-discovery-disagreement

Conversation

@njbrake

@njbrake njbrake commented Jul 23, 2026

Copy link
Copy Markdown
Member

Description

/v1/models discovery and completion routing disagreed on what a "configured provider" is. Completions work for any any-llm provider once its native credential env var is set (get_provider_kwargs returns {} for a provider absent from config.providers, so any-llm reads the env var directly), but model discovery only queried config.providers. Setting only ANTHROPIC_API_KEY therefore made Anthropic completions work while its models never appeared in GET /v1/models, the exact quick path the Railway template recommends.

Discovery now also picks up any-llm providers made callable by an env var alone: a provider whose credential env var is present and that supports live model listing is treated as discoverable under its implementation name (the same provider:model selector routing resolves), unless it is already a configured instance. This is wired into both GET /v1/models and GET /v1/models/discoverable (and the ?provider= filter), so the two surfaces agree with what completions can reach. Provider-health and the per-instance connection test are untouched; they operate on explicit or configured instances.

Also restores os.environ around each test in test_config_env_loading, whose python-dotenv loads leaked provider credentials into the process. That was harmless before, but now that discovery is env-aware it polluted an unrelated test; a pre-existing isolation gap the change surfaced.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #221

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (no contract change; --check confirms the spec is up to date).

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

Claude Code (Opus 4.8), driven by @njbrake.

Any additional AI details you'd like to share:

Implemented and tested end to end by Claude via back-and-forth with @njbrake; the reasoning and decisions are his.

NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)

  • I am an AI Agent filling out this form (check box if true)

Completion routing works for any any-llm provider once its native
credential env var is set (get_provider_kwargs returns {} for a provider
absent from config.providers, so any-llm reads the env var directly), but
model discovery only queried config.providers. Setting only
ANTHROPIC_API_KEY therefore made Anthropic completions work while its
models never appeared in GET /v1/models, so the two surfaces silently
disagreed on what a configured provider is.

Discovery now also picks up any-llm providers made callable by an env var
alone: a provider whose credential env var is present and that supports
live model listing is treated as discoverable under its implementation
name, the same selector routing resolves. This keeps GET /v1/models and
GET /v1/models/discoverable consistent with what completions can reach.

Also restore os.environ around each test in test_config_env_loading, whose
python-dotenv loads leaked provider credentials into the process and, now
that discovery is env-aware, into unrelated tests.

Fixes #221

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake had a problem deploying to integration-tests July 23, 2026 13:07 — with GitHub Actions Failure
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fcdfe80c-7239-433e-9e79-0af32b1d1dad

📥 Commits

Reviewing files that changed from the base of the PR and between fa68234 and 2b79d3e.

📒 Files selected for processing (7)
  • docs/configuration.md
  • docs/models.md
  • src/gateway/services/model_discovery_service.py
  • tests/integration/test_config_env_loading.py
  • tests/integration/test_model_discovery.py
  • tests/unit/test_gateway_model_discovery.py
  • tests/unit/test_model_discovery_env_providers.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/models-discovery-disagreement
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/models-discovery-disagreement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

A blank provider credential env var (for example ANTHROPIC_API_KEY=,
common from container templating) was counted as present, so the provider
was added to discovery only to fail authentication, surfacing a spurious
error row in GET /v1/models/discoverable. Require a non-empty value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a mismatch between completion routing and model discovery by treating any-llm providers as “discoverable” when their native credential environment variable is set, even if they are not explicitly listed under config.providers. This makes /v1/models (and the operator-facing discovery/status endpoints) align with what completions can actually reach.

Changes:

  • Extend model discovery to include env-credentialed any-llm providers (when they support live model listing) and apply this consistently to /v1/models, /v1/models/discoverable, and provider filtering.
  • Add unit and integration regression tests covering env-only provider discovery (issue #221) and harden env isolation in config env-loading tests.
  • Update documentation to reflect that env-only credentials affect both routing and model discovery.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/gateway/services/model_discovery_service.py Adds env-var-based provider detection and uses it to build the discoverable instance list for discovery and status surfaces.
tests/unit/test_model_discovery_env_providers.py New unit tests validating env-only provider detection, ordering, filtering, and status inclusion.
tests/unit/test_gateway_model_discovery.py Pins existing discovery tests to configured providers by stubbing env-provider detection.
tests/integration/test_model_discovery.py Adds an integration regression test asserting env-only providers appear in GET /v1/models.
tests/integration/test_config_env_loading.py Restores os.environ after each test to prevent dotenv-loaded provider creds from leaking across tests.
docs/models.md Documents that env-only provider credentials affect both routing and discovery.
docs/configuration.md Updates model_discovery setting description to include env-only discoverable providers.

Comment thread src/gateway/services/model_discovery_service.py

@khaledosman khaledosman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving. Detailed points are inline (see the follow-up review). Themes:

  • _env_provider_instances dedups by instance name, not implementation — a configured instance plus its matching credential env var can double-discover the same provider.
  • AnyLLM.get_all_provider_metadata() is the one unguarded external call in an otherwise defensive module; a raise there 500s /v1/models.
  • Env edge cases (slash-alternatives, compound, "None", blank) verified correct; no new capability exposed. Tests are solid bar the same-implementation duplication case.

@khaledosman khaledosman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up: converting the earlier prose review into inline comments; see threads on model_discovery_service.py and the test files. No blockers.

Retraction: my earlier cross-PR concern (that this PR's Railway/PaaS promotion collides with a Secure-cookie downgrade in #384) was based on a misread of #384 against main. #384 honors X-Forwarded-Proto via request_is_https(), so the proxy case is handled and the concern does not apply. Withdrawn.

Comment thread src/gateway/services/model_discovery_service.py Outdated
Comment thread src/gateway/services/model_discovery_service.py Outdated
Comment thread src/gateway/services/model_discovery_service.py Outdated
Comment thread src/gateway/services/model_discovery_service.py
Comment thread src/gateway/services/model_discovery_service.py
Comment thread tests/unit/test_model_discovery_env_providers.py
Address review on #388.

Deduplicate env-based discovery against the implementations the configured
instances resolve to, not their instance names. A custom instance such as
my-anthropic (provider_type: anthropic) alongside ANTHROPIC_API_KEY would
otherwise also yield a bare "anthropic", dialing the same credential twice
and listing the same models under two keys.

Guard AnyLLM.get_all_provider_metadata() so a registry hiccup falls back to
configured-only discovery instead of 500-ing GET /v1/models and
/v1/models/discoverable, matching the module's defensive posture.

Document that keyless local providers (ollama/llamacpp/llamafile) have no
credential signal to detect and stay callable-but-undiscoverable, tracked
as a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 23, 2026 14:13 — with GitHub Actions Inactive
Address Copilot review on #388. Strip the credential env var value before
the truthiness check so a whitespace-only value (as unusable as a blank one,
and common from container templating) does not mark a provider discoverable
only for it to fail authentication.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 23, 2026 14:21 — with GitHub Actions Inactive
@njbrake
njbrake merged commit 340bd4b into main Jul 23, 2026
5 checks passed
@njbrake
njbrake deleted the fix/models-discovery-disagreement branch July 23, 2026 14:25
Sourav-Tripathy pushed a commit to Sourav-Tripathy/otari that referenced this pull request Jul 23, 2026
…lla-ai#384)

* fix(dashboard): persist sign-in with an HttpOnly session cookie

The dashboard forced operators to re-paste the master key on every new
tab, closed tab, or browser restart, because the raw key lived in
per-tab sessionStorage. Signing in now exchanges the key once
(POST /v1/auth/session) for a server-issued opaque session held in an
HttpOnly cookie, which is both more convenient (survives tabs and
restarts) and more secure (no JS-readable credential, TTL, server-side
revocation).

Backend:
- dashboard_sessions table stores only SHA-256 token hashes, so every
  worker and replica accepts a session and revocation is global.
- POST /v1/auth/session verifies the master key and sets the cookie
  (HttpOnly; SameSite=Strict; Secure mirrors the request scheme so
  plain-HTTP LAN deployments keep working); DELETE signs out.
- The master-key auth dependencies accept the cookie only when a
  request carries no header credentials, so API clients are untouched.
  A Sec-Fetch-Site check backs up SameSite as CSRF defense in depth.
- Master-key rotation revokes every session and re-mints the caller's
  cookie on the response, so the rotating tab stays signed in.
- Session TTL is configurable via dashboard_session_ttl_hours
  (default 7 days).

Frontend:
- The raw key is sent once to mint the session and never stored; a
  non-secret localStorage marker keeps the signed-in state synchronous,
  and any 401 drops it exactly like a mid-session revocation.
- Sign-out revokes the session server-side before clearing local state.

Fixes mozilla-ai#338

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): revoke sessions when the master key changes at startup

Two follow-ups from self-review of the session-cookie change:

- A dashboard session only proves possession of the master key at mint
  time, so it must not outlive the key. The generated key already
  revoked sessions inline at rotation, but a configured key rotates by
  changing OTARI_MASTER_KEY and restarting, which no request handler
  observes; pre-rotation sessions survived until their TTL. Store a
  hash of the effective key in runtime_settings and revoke every
  session at startup when it changes (covering configured-key rotation
  and configured/generated regime switches). The rotation endpoint
  keeps the marker in step so its re-minted session survives restarts.

- Regenerate the Postman collection from the updated OpenAPI spec
  (make postman), which the openapi-spec CI job checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): keep sign-out best-effort when revocation fails

Raising a 500 on a DB failure during DELETE /v1/auth/session made
FastAPI discard the injected response, so the cookie was never cleared
while the frontend had already dropped its local marker: the browser
kept a live session cookie the operator believed was gone. Sign-out now
always clears the cookie and returns 204, logging the failed revocation;
the unrevoked row dies on its TTL.

Flagged by Copilot review on mozilla-ai#384.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): honor X-Forwarded-Proto for the session cookie Secure flag

Behind a TLS-terminating proxy, uvicorn only trusts X-Forwarded-Proto
from loopback by default, so request.url.scheme reads "http" on typical
PaaS ingress and the session cookie was minted without Secure despite an
HTTPS browser leg. Decide the Secure attribute from the effective scheme
(direct https, or the first X-Forwarded-Proto value). Honoring the
header regardless of source is safe for this decision alone: a spoofed
"https" over plain HTTP only denies the spoofer their own cookie.

Also covers the Secure=true branch in tests (direct HTTPS and forwarded
proto), and documents two review-noted asymmetries: DELETE skipping the
Sec-Fetch-Site check (SameSite already covers it; worst case is a forced
sign-out) and Path=/ (management routes live beside inference under /v1,
and the cookie grants nothing beyond master authority).

Addresses khaledosman's review on mozilla-ai#384.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): log sign-in session persistence failures

The create_session 500 branch discarded the SQLAlchemyError without a
trace, unlike the parallel branch in delete_session; a sign-in outage
would have been invisible in the logs. Flagged by CodeRabbit on mozilla-ai#384.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(dashboard): regenerate bundle and API artifacts after rebase

Rebuild the committed dashboard bundle, OpenAPI spec, and Postman
collection from the tree rebased onto main, folding in main's dashboard
and API changes (mozilla-ai#383, mozilla-ai#385, mozilla-ai#388).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@njbrake njbrake mentioned this pull request Jul 24, 2026
4 tasks
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.

/v1/models discovery and completion routing disagree on what a "configured provider" is

3 participants