Skip to content

feat(providers): live model lists for OpenRouter and OpenGateway - #860

Open
euxaristia wants to merge 5 commits into
Gitlawb:mainfrom
euxaristia:euxaristia/live-model-list-openrouter-opengateway
Open

feat(providers): live model lists for OpenRouter and OpenGateway#860
euxaristia wants to merge 5 commits into
Gitlawb:mainfrom
euxaristia:euxaristia/live-model-list-openrouter-opengateway

Conversation

@euxaristia

@euxaristia euxaristia commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

OpenRouter and OpenGateway now load their public live model lists the way cairn-code does, so the picker stays current without the broken static OpenGateway catalog or a models.dev-only OpenRouter path.

Changes

internal/providermodelcatalog

  • Point OpenGateway remote catalog at live GET {base}/v1/models (replacing dead /zero/models.json)
  • Fetch OpenRouter from live GET {base}/models instead of models.dev alone
  • Parse context length, architecture modalities, tools/reasoning flags, and free markers from aggregator payloads
  • Trust the OpenGateway list as operator-curated; keep coding filters for OpenRouter

internal/providermodeldiscovery

  • Probe OpenRouter/OpenGateway public model lists without requiring an API key
  • Prefer live aggregator lists at merge time instead of intersecting away live-only IDs
  • Capture context windows and free labels from live /models responses
  • Raise live response body limit to 4MiB for large OpenRouter catalogs

Tests

  • Cover URL defaults, OpenRouter live parse, OpenGateway auto route retention, unauthenticated OpenGateway discovery, and OpenRouter live-only merge

Test plan

  • go test ./internal/providermodelcatalog/ ./internal/providermodeldiscovery/ -count=1
  • go vet ./internal/providermodelcatalog/ ./internal/providermodeldiscovery/
  • gofmt -l clean on touched packages
  • go test ./internal/providercatalog/ ./internal/tui/ -count=1
  • go run ./cmd/zero-release build
  • Manual: open /model with OpenGateway key unset → live routes including auto load from GET /v1/models
  • Manual: open /model with OpenRouter key set → full live coding list with context windows

Fixes #859

Summary by CodeRabbit

  • New Features

    • Added live model catalog support for OpenRouter and OpenGateway.
    • Model discovery now works without credentials for supported public catalogs.
    • Added richer model details, including context limits, pricing, tools, modalities, reasoning, architecture, descriptions, and free-model indicators.
    • OpenRouter endpoints can be customized.
  • Bug Fixes

    • Improved endpoint handling and fallback behavior when live responses fail.
    • Excluded unsupported non-coding models from coding catalogs.
    • Preserved live-only models and filled missing catalog metadata when available.

Prefer each aggregator public /v1/models catalog over the dead OpenGateway
models.json path and models.dev-only OpenRouter mapping so the picker shows
current routes with context windows and free labels.

Refs Gitlawb#859
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de727812-04b9-4739-b799-a698f16eb9bc

📥 Commits

Reviewing files that changed from the base of the PR and between fb43a48 and 76f2429.

📒 Files selected for processing (5)
  • internal/providermodelcatalog/logic_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodeldiscovery/discovery.go
  • internal/providermodeldiscovery/discovery_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/providermodeldiscovery/discovery_test.go
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodeldiscovery/discovery.go

Walkthrough

OpenRouter and OpenGateway now fetch public live model lists, parse expanded metadata, and merge eligible live-only models into discovery. OpenRouter supports configurable URLs and models.dev fallback behavior.

Changes

Live provider catalogs

Layer / File(s) Summary
Remote catalog fetching and routing
internal/providermodelcatalog/remote.go, internal/providermodelcatalog/logic_test.go
Remote fetching routes OpenRouter and OpenGateway requests to derived /models endpoints. OpenRouter falls back to models.dev after HTTP or parsing failures.
Catalog parsing and metadata conversion
internal/providermodelcatalog/remote.go, internal/providermodelcatalog/remote_test.go
Catalog parsing filters non-coding models where required and maps context, modalities, tools, reasoning, pricing, architecture, display-name, and free-model metadata.
Public catalog discovery and merging
internal/providermodeldiscovery/discovery.go, internal/providermodeldiscovery/discovery_test.go
Discovery probes public catalogs without credentials, preserves live metadata, and retains eligible live-only models during merging. Tests cover both providers and metadata parsing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiscoverCatalog
  participant FetchRemote
  participant PublicCatalog
  participant ModelParser
  participant MergeCatalogModels
  DiscoverCatalog->>FetchRemote: request public live catalog
  FetchRemote->>PublicCatalog: GET /v1/models
  PublicCatalog-->>FetchRemote: model payload
  FetchRemote->>ModelParser: parse model metadata
  ModelParser-->>FetchRemote: filtered live models
  FetchRemote-->>DiscoverCatalog: return live catalog
  DiscoverCatalog->>MergeCatalogModels: merge live and curated models
  MergeCatalogModels-->>DiscoverCatalog: return discovered models
Loading

Possibly related issues

  • Issue 2083: The change covers live OpenRouter and OpenGateway catalog discovery, metadata enrichment, filtering, and fallback behavior.

Suggested reviewers: kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: live model lists for OpenRouter and OpenGateway.
Linked Issues check ✅ Passed The changes implement live catalogs, keyless discovery, metadata preservation, merging, filters, and fallback behavior required by [#859].
Out of Scope Changes check ✅ Passed All changes support issue #859 and remain within the scoped providermodelcatalog and providermodeldiscovery packages.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providermodelcatalog/remote.go`:
- Around line 207-230: The remoteModel pricing representation must also accept
OpenRouter’s string-valued pricing.prompt and pricing.completion fields. Add
uniquely tagged pricing fields to remoteModel, then update toModel to parse and
map them into InputCost and OutputCost while preserving the existing
cost.input/cost.output mapping as fallback where appropriate.

In `@internal/providermodeldiscovery/discovery_test.go`:
- Around line 55-84: Fix the sort-order assertion in
TestParseModelsResponseCapturesContextAndFree by directly comparing the ordered
model IDs as a joined list against the expected alphabetical sequence. Remove
the nested conditional and ensure the assertion fails when either model appears
at the wrong index.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b6a21cdf-551f-42c7-af9e-92f2d6eab0d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and 7a3be2a.

📒 Files selected for processing (5)
  • internal/providermodelcatalog/logic_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodeldiscovery/discovery.go
  • internal/providermodeldiscovery/discovery_test.go

Comment thread internal/providermodelcatalog/remote.go
Comment thread internal/providermodeldiscovery/discovery_test.go
…ertions

Parse OpenRouter prompt and completion string pricing fields into InputCost and OutputCost, and directly assert alphabetical model sort order in discovery tests.

Refs Gitlawb#859
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this — the bug is real. I confirmed https://opengateway.gitlawb.com/zero/models.json returns 404 while /v1/models returns 200 unauthenticated, so the catalog really was pointing at nothing, and pulling auto back into the picker is the right fix.

Two things before it goes in.

Pricing lands a factor of a million out. internal/providermodelcatalog/remote.go:455parsePricingString returns OpenRouter's and OpenGateway's pricing.prompt verbatim, but those fields are USD per token, while Model.InputCost/OutputCost everywhere else in the codebase (models.dev cost.input) is USD per million tokens. providerWizardModelMeta renders it with $%g/%g (internal/tui/provider_wizard_models.go:44-46), so running the real live payloads through the parser and the picker formatter gives:

this branch: anthropic/claude-opus-4.8 | 1000000 ctx · tools · reasoning · $5e-06/2.5e-05
main:        openai/gpt-4.1            | 1048576 ctx · tools · $2/8

That's all 303 OpenRouter models and the 10 priced OpenGateway models showing scientific notation a millionth of the real price, in the same list as correctly-scaled per-million prices from every other provider.

return val * 1e6 fixes it. Worth knowing that when I applied it the only failing test was remote_test.go:202, which currently pins the wrong unit (want 0.000003/0.000015) — so the test agrees with the bug today. Bumping it to 3/15 matches what models.dev reports for the same model and the suite goes green.

OpenRouter lost its independent fallback. With models.dev out of the openrouter path (remote.go:35-48), both the catalog fetch and the live probe now hit openrouter.ai, so if that host is unreachable the picker errors out entirely instead of degrading. Two httptest servers, openrouter returning 502 and models.dev healthy: main returns 1 model and a nil error, this branch returns 0 models and models endpoint returned 502 Bad Gateway. Falling back to FetchModelsDev when the live fetch fails would keep the old resilience.


Two notes, no action needed. For these two providers fetchCatalogModels and Discover now GET the same /models URL, so opening the picker once makes two identical requests — about 0.5MB twice on OpenRouter, uncached; one could reuse the other's body. And five OpenRouter models publish pricing.prompt: "-1" as their variable/BYOK marker, which parsePricingString maps to 0 and renders as no price. Harmless, worth a comment so it doesn't read as a parse failure later.


Things I went looking for specifically and found clean, since this is a provider-catalog change:

  • No attribution or affiliate headers. The only headers this diff sends are Accept: application/json and User-Agent: zero-cli, both pre-existing. No HTTP-Referer, no X-Title, no app tag, no query parameter the user didn't configure.
  • No credential on the public catalog fetch. FetchOpenRouter/FetchOpenGateway go through fetchJSON, which sends no auth — recording the requests with a key configured shows the catalog fetch carrying an empty Authorization; only the live probe sends the key. Redaction still holds: a 401 whose body echoes the header comes back as bad key: Bearer [REDACTED].
  • Trust anchors. Both hardcoded URLs are reachable unauthenticated and are the obvious hosts — the project's own gateway and the official OpenRouter API. No new third party introduced.
  • Sensible limits: 4MiB cap against a real 529KB payload, 10s client timeout, 8s context in the picker, and discovery still degrades to the catalog rather than erroring the command.

go build ./..., go vet and gofmt -l are clean on the touched packages, and internal/tui and internal/providercatalog pass. Everything above is in one pass — nothing queued behind it.

Convert OpenRouter/OpenGateway per-token pricing to per-million for Model costs, and fall back to models.dev when the live OpenRouter fetch fails so the picker still degrades.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed both review blockers:

  1. Pricing unitparsePricingString multiplies live per-token pricing.prompt/completion by 1e6 so InputCost/OutputCost match models.dev (USD per million tokens). Test updated to 3/15. Comment notes OpenRouter "-1" BYOK markers map to 0.

  2. OpenRouter fallback — live fetch failure falls back to FetchModelsDev("openrouter") so a down openrouter.ai host still yields a coding list. Covered by TestFetchOpenRouterFallsBackToModelsDev.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Aug 2, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Re-checked on b6c1832d rather than taking the summary's word for it, and both blockers are properly closed.

Pricing. return val * 1e6 is right, and more importantly the test now pins the unit instead of agreeing with the bug. I deleted the scale and TestParseOpenRouterCatalogMapsLiveMetadata goes red with claude costs = 0.000003/0.000015, want 3/15 (per-million, matching models.dev) — so the thing that made this bug invisible before is now the thing that catches it. Good call updating remote_test.go:203 to the real unit rather than leaving it pinned to the old value.

Fallback. FetchModelsDev(ctx, "openrouter", options) on live-fetch failure restores the resilience that was lost, so a down openrouter.ai degrades to models.dev instead of erroring the picker.

internal/providermodelcatalog and internal/providermodeldiscovery are both green here.

Thanks for the "-1" BYOK comment too — that was a throwaway note in my review and you picked it up anyway. It'll stop the next reader mistaking a deliberate marker for a parse failure.

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providermodelcatalog/remote.go`:
- Around line 75-85: Update FetchOpenRouter in
internal/providermodelcatalog/remote.go (lines 75-85) to handle
ParseOpenRouterCatalog errors through the existing FetchModelsDev fallback,
returning fallback models when parsing fails and preserving combined error
reporting if fallback also fails. Add a regression case in
internal/providermodelcatalog/logic_test.go (lines 171-208) with a successful
HTTP response containing malformed JSON, verifying models.dev data is returned.

In `@internal/providermodeldiscovery/discovery_test.go`:
- Around line 83-122: Update
TestDiscoverCatalogOpenGatewayUsesLiveListWithoutKey in
internal/providermodeldiscovery/discovery_test.go:83-122 to make the catalog
endpoint return a distinct list that omits live-only-model, while the live
/v1/models response retains and asserts it. Update the adjacent test in
internal/providermodeldiscovery/discovery_test.go:125-169 similarly so its
catalog omits anthropic/claude-sonnet-4.5, the live response retains it, and the
profile has no APIKey to cover unauthenticated OpenRouter discovery.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b762bf74-03ef-46b1-8161-6e705c575c89

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and b6c1832.

📒 Files selected for processing (5)
  • internal/providermodelcatalog/logic_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodeldiscovery/discovery.go
  • internal/providermodeldiscovery/discovery_test.go

Comment thread internal/providermodelcatalog/remote.go
Comment thread internal/providermodeldiscovery/discovery_test.go
… merge

Route malformed OpenRouter HTTP 200 bodies through the models.dev fallback,
and exercise catalog vs live payload split so live-only models stay merged.
@euxaristia

Copy link
Copy Markdown
Contributor Author

CodeRabbit follow-up

Addressed open findings from comments 3737946220 and 3737946225.

Changes (fb43a483)

  1. FetchOpenRouter: HTTP 200 with unparseable body now uses the same models.dev fallback as the transport-failure path; combined error preserved when fallback also fails.
  2. TestFetchOpenRouterFallsBackOnMalformedJSON: regression for valid HTTP 200 + malformed JSON returning models.dev data.
  3. Discovery tests: catalog and live endpoints return distinct payloads (catalog omits live-only model; live retains it). Assert live-only ids remain after merge. OpenRouter path runs unauthenticated (no API key).

Tests

go test ./internal/providermodelcatalog/ ./internal/providermodeldiscovery/ -count=1
ok

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 13 minutes.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providermodelcatalog/logic_test.go`:
- Around line 51-70: The URL derivation tests do not cover removing query
strings and fragments from a base URL. Extend the relevant defaulted URL test,
such as TestDefaultedOpenGatewayURL or TestDefaultedOpenRouterURL, with a base
URL containing both query and fragment values, and assert the derived /models
URL excludes them.

In `@internal/providermodelcatalog/remote.go`:
- Around line 466-477: Update parsePricingString to reject non-finite parsed
values, including NaN and positive or negative infinity, before multiplying by
1e6; retain the existing zero result for invalid or non-positive inputs. Add
regression coverage for "Inf" and "NaN" confirming both return zero.

In `@internal/providermodeldiscovery/discovery_test.go`:
- Around line 55-80: Update TestParseModelsResponseCapturesContextAndFree so the
free model fixture’s name does not contain “free”, then assert that its parsed
Description contains the expected free-model annotation. Keep the assertion tied
to the model’s is_free behavior rather than relying on the input name.

In `@internal/providermodeldiscovery/discovery.go`:
- Around line 329-340: Retain capability-marked OpenRouter live-only models by
decoding tool, supported-parameter, and reasoning fields in modelsResponse,
mapping them into Model.ToolCall and Model.Reasoning in the live conversion
flow, and using IsCodingModel(catalogModelFromDiscovery(live)) in the live-only
merge filter instead of only the ID heuristic. In
internal/providermodeldiscovery/discovery_test.go lines 137-192, add a
generic-ID live fixture with supported_parameters ["tools"] and assert it
remains in the merged result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f64ceae-427f-45f2-90f9-a7b1382f6268

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and fb43a48.

📒 Files selected for processing (5)
  • internal/providermodelcatalog/logic_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodeldiscovery/discovery.go
  • internal/providermodeldiscovery/discovery_test.go

Comment thread internal/providermodelcatalog/logic_test.go
Comment thread internal/providermodelcatalog/remote.go
Comment thread internal/providermodeldiscovery/discovery_test.go
Comment thread internal/providermodeldiscovery/discovery.go Outdated
Reject non-finite OpenRouter pricing, keep capability-marked live-only
models, and harden related regression tests.
@euxaristia

Copy link
Copy Markdown
Contributor Author

CodeRabbit follow-up (76f2429)

Addressed the four remaining open threads:

  1. URL derivation test — cover query/fragment stripping in defaultedOpenAIModelsURL
  2. parsePricingString — reject non-finite (NaN/Inf) pricing values + regression cases
  3. Free-model test — fixture name no longer embeds free; assert exact (free) annotation
  4. MAJOR: OpenRouter live-only — decode tool/reasoning/supported_parameters on live responses; filter live-only via IsCodingModel so capability-marked generic IDs are kept

Validated: go test ./internal/providermodelcatalog/ ./internal/providermodeldiscovery/

Not requesting @coderabbitai full review — rate-limit cooldown / email flood; please re-review when headroom returns.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

@euxaristia have exceeded the limit for the number of chat messages per hour. Please wait 6 minutes and 57 seconds before sending another message.

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.

feat(providers): live model list for OpenRouter and OpenGateway

2 participants