Skip to content

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

Open
euxaristia wants to merge 3 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 3 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 can now work 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.
    • 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: 36831eff-edad-4c74-9466-2709cc5031ed

📥 Commits

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

📒 Files selected for processing (3)
  • internal/providermodelcatalog/logic_test.go
  • internal/providermodelcatalog/remote.go
  • internal/providermodelcatalog/remote_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/providermodelcatalog/remote_test.go
  • internal/providermodelcatalog/remote.go

Walkthrough

OpenRouter and OpenGateway now fetch public live model lists, parse expanded metadata, and integrate live-only models into discovery. URL derivation uses OpenAI-compatible /models endpoints, with configurable OpenRouter overrides and fallback catalog support.

Changes

Live provider catalogs

Layer / File(s) Summary
Remote catalog fetching and parsing
internal/providermodelcatalog/remote.go, internal/providermodelcatalog/logic_test.go, internal/providermodelcatalog/remote_test.go
OpenRouter fetching and parsing were added. OpenRouter falls back to models.dev after upstream errors. OpenGateway parsing handles /models payloads, non-coding exclusions, and free-model annotations. Model conversion preserves context, modalities, tools, reasoning, pricing, and architecture metadata.
Live catalog discovery and merging
internal/providermodeldiscovery/discovery.go, internal/providermodeldiscovery/discovery_test.go
Discovery probes public OpenRouter and OpenGateway catalogs without credentials. It preserves descriptions, context windows, free indicators, and live status. Public catalogs can supply live-only models, while provider-specific filters remain active. Tests cover both providers.

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 models and 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 implements live OpenRouter and OpenGateway /v1/models catalogs, filtering, metadata preservation, and live-only model merging.

Suggested reviewers: kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 live model-list changes for OpenRouter and OpenGateway.
Linked Issues check ✅ Passed The changes implement issue #859 objectives, including live catalogs, metadata preservation, filtering, fallback behavior, and live-only model merging.
Out of Scope Changes check ✅ Passed The changes remain within the scoped internal/providermodelcatalog and internal/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 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.

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