feat(provider): fetch models from OpenAI-compatible providers in Settings#1474
Conversation
…ings Add a "Fetch models" action to each OpenAI-compatible provider group in Settings > Models. It calls the provider's live /models endpoint using the provider's already-configured base URL, auth, and headers, then merges any models the catalog/config does not already list into config.provider.<id>.models. New models land disabled (visibility default) so a large gateway never floods the picker. Resolves the gap behind issue #1463, where models.dev's static catalog lagged a live Kilo Gateway key (missing stepfun/step-3.7-flash:free). Server: provider/fetch-models.ts pure parse + request-resolution helpers; fetchProviderModels action (base URL precedence config > catalog, Bearer from auth store, 10s timeout, mapped errors, keys never logged); POST /provider/:providerID/models route + handler; regenerated openapi.json and v2 SDK client. App: per-group Fetch models button reusing the in-house refresh icon, mergeFetchedModels helper, en/zh i18n, settings-models-fetch snap target. Tests: parse / request-resolution / merge unit tests; openapi drift guard passes. Refs #1463
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds live model discovery for OpenAI-compatible providers. A new ChangesFetch Models for OpenAI-Compatible Providers
Sequence Diagram(s)sequenceDiagram
rect rgba(100, 149, 237, 0.5)
note over User,globalSync: Frontend
User->>SettingsModels: Click "Fetch models"
SettingsModels->>SettingsModels: Set fetchingID (disable button)
end
rect rgba(144, 238, 144, 0.5)
note over SettingsModels,ProviderEndpoint: Backend discovery
SettingsModels->>fetchProviderModels: POST /provider/:providerID/models
fetchProviderModels->>fetchProviderModels: Resolve config + auth + catalog
fetchProviderModels->>ProviderEndpoint: GET /models (10 s timeout)
ProviderEndpoint-->>fetchProviderModels: JSON response
fetchProviderModels->>FetchModels: parse(json)
FetchModels-->>fetchProviderModels: Parsed[]
fetchProviderModels-->>SettingsModels: { ok: true, models } or { ok: false, message }
end
rect rgba(100, 149, 237, 0.5)
note over SettingsModels,globalSync: Merge and persist
SettingsModels->>mergeFetchedModels: merge(existing, config, fetched)
mergeFetchedModels-->>SettingsModels: { models, addedModelIDs, skipped }
SettingsModels->>globalSync: updateConfig(merged models)
SettingsModels->>User: showToast (success / no-new / error)
SettingsModels->>SettingsModels: Clear fetchingID
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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. Comment |
There was a problem hiding this comment.
Suggested priority: P2 (includes user-path files (packages/app/src/components/settings-models-fetch.test.ts, packages/app/src/components/settings-models-fetch.ts, packages/app/src/components/settings-models.tsx, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts)).
P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/opencode/src/server/instance/provider-actions.ts (1)
107-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
HttpClient.HttpClientin this Effect service path instead of rawfetch.This keeps transport concerns (timeouts/retries/tracing/middleware) aligned with the rest of the Effect service layer.
As per coding guidelines, “Prefer
HttpClient.HttpClientinstead of rawfetchin Effect services”.🤖 Prompt for 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. In `@packages/opencode/src/server/instance/provider-actions.ts` around lines 107 - 110, Replace the raw fetch call in the FetchProviderModelsResult promise with HttpClient.HttpClient from the Effect framework. Remove the async promise wrapper and instead use HttpClient to make the HTTP request to FetchModels.endpoint(baseURL) with the headers and timeout configuration. This aligns the transport layer with Effect service conventions and ensures consistent handling of timeouts, retries, and tracing across the codebase.Source: Coding guidelines
🤖 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 `@packages/app/src/components/settings-models.tsx`:
- Around line 55-56: The issue is that while the check at line 55 prevents
concurrent fetches globally using fetchingID(), the button disable logic at line
162-163 only disables the button for the active provider being fetched. This
allows other provider buttons to remain clickable even when a fetch is already
in progress elsewhere. To fix this, update the button disable condition to check
if fetchingID() has any truthy value (meaning any fetch is in progress), and if
so, disable all provider buttons, not just the currently active one. This
ensures consistent behavior where no buttons are clickable while any fetch
operation is ongoing.
In `@packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts`:
- Around line 112-114: In the error handling block for the fetchProviderModels
call, instead of hardcoding the status as 400 in the
HttpServerResponse.jsonUnsafe response, use the status value from the result
object (result.status). This preserves the specific error status computed by
fetchProviderModels, whether it's a timeout, upstream error, or other failure,
rather than collapsing all errors to a generic 400 response. Ensure the
HttpApi/OpenAPI error status declarations are then updated to reflect the actual
propagated statuses.
---
Nitpick comments:
In `@packages/opencode/src/server/instance/provider-actions.ts`:
- Around line 107-110: Replace the raw fetch call in the
FetchProviderModelsResult promise with HttpClient.HttpClient from the Effect
framework. Remove the async promise wrapper and instead use HttpClient to make
the HTTP request to FetchModels.endpoint(baseURL) with the headers and timeout
configuration. This aligns the transport layer with Effect service conventions
and ensures consistent handling of timeouts, retries, and tracing across the
codebase.
🪄 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: f31597ab-56be-4381-8710-0b12ec05cccd
⛔ Files ignored due to path filters (2)
packages/sdk/js/src/v2/gen/sdk.gen.tsis excluded by!**/gen/**packages/sdk/js/src/v2/gen/types.gen.tsis excluded by!**/gen/**
📒 Files selected for processing (12)
packages/app/e2e/snap/settings-models-fetch.snap.tspackages/app/src/components/settings-models-fetch.test.tspackages/app/src/components/settings-models-fetch.tspackages/app/src/components/settings-models.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/opencode/src/provider/fetch-models.test.tspackages/opencode/src/provider/fetch-models.tspackages/opencode/src/server/instance/provider-actions.tspackages/opencode/src/server/routes/instance/httpapi/groups/provider.tspackages/opencode/src/server/routes/instance/httpapi/handlers/provider.tspackages/sdk/openapi.json
…r path Address code review on the Settings > Models fetch action (#1463). P1 — new models were visible by default. A freshly fetched model carries no release_date, and the model visibility default treats undated models as visible (createModelsView.visible), so a large gateway flooded the picker — the opposite of the intended "new models start disabled". mergeFetchedModels now returns addedModelIDs, and the handler marks exactly those hidden (setVisibility false) before persisting config, so the picker stays clean until the user opts a model in. P3 — concurrency guard / button state mismatch: a fetch globally blocks others (if fetchingID return), but only the active provider's button was disabled, so other Fetch buttons looked clickable and silently no-op'd. All Fetch buttons now disable while any fetch is in flight; loading text stays on the active provider. P3 — half-implemented error pipeline: the action computed per-error status codes (400/502/504) that nothing consumed (handler always returns 400, OpenAPI declares only 400, UI shows message only). Dropped the status field; the message already carries the human-readable cause. Wire contract unchanged, so openapi.json/SDK are untouched. Verify: opencode typecheck clean; app typecheck clean; fetch-models 13 pass; settings-models-fetch + models contract 11 pass; openapi drift guard 7 pass; eslint 0 errors; git diff --check clean.
…odels The header comment claimed newly fetched models "land disabled (visibility default)", but the visibility default treats undated models as visible — the disabling is done explicitly by the setVisibility(false) loop, which already carries the accurate explanation. Remove the inaccurate duplicate. Comment-only.
Summary
Add a Fetch models action to each OpenAI-compatible provider group in Settings > Models. It calls the provider's live
/modelsendpoint using the provider's already-configured base URL, auth, and headers, parses the returned model IDs, and merges any the catalog/config does not already list intoconfig.provider.<id>.models. Newly added models are explicitly marked hidden, so a large gateway never floods the picker — the user toggles on the ones they want.provider/fetch-models.tspure helpers (parsefor{data:[]}/{models:[]}/ bare-array shapes with dedup;requestfor base-URL precedence + Bearer header;endpoint);fetchProviderModelsaction;POST /provider/:providerID/modelsroute + handler; regeneratedopenapi.jsonand the v2 SDK client (client.provider.fetchModels).settings-models.tsxreusing the in-houserefreshicon;mergeFetchedModelshelper; en/zh i18n; a focusedsettings-models-fetchsnap target.Why
Reported in #1463: with the same Kilo Gateway API key, LobeHub listed models PawWork did not (e.g.
stepfun/step-3.7-flash:free). PawWork's model list for a recognized gateway like Kilo comes from the staticmodels.devcatalog, which lags what the gateway actually serves live. This adds the missing path — fetch the gateway's current/modelson demand and merge in what the catalog is missing — without building a full model database. The base URL is resolved from the user's config override when present, otherwise themodels.devcatalog entry, so a provider connected with just an API key (the reporter's case) works without re-entering anything.Related Issue
Resolves #1463
Human Review Status
Pending
Review Focus
fetchProviderModelsresolution + error mapping (provider-actions.ts): base-URL precedence (configendpoint/baseURL→models.devapi), Bearer injected from the auth store (never overwriting an explicitAuthorizationheader), 10s timeout, and that API keys are never logged.mergeFetchedModelsonly writes models the provider does not already expose, preserves existing config entries, andconfig.update'smergeDeepmakes the write additive/idempotent. A config-added model inherits the gateway's base URL and npm from the catalog (provider.ts"extend database from config"), so only{ name }is persisted per model.release_date, and the model visibility default treats undated models as visible (createModelsView.visible). SomergeFetchedModelsreturnsaddedModelIDsand the handler marks exactly those hidden (setVisibility(false)) before persisting config — without this, a large gateway would flood the picker. Confirmed by the existing visibility contract incontext/models.test.ts.mergeFetchedModelsreturns the correctaddedModelIDs(its own test), and undated +hidepref → not visible (context/models.test.ts). The remaining seam is the two-line application infetchModels(thesetVisibility(false)loop +updateConfig), guarded by typecheck. We deliberately did not add a mounted-and-mocked component test: this codebase tests config-writing flows by extracting a pure function and testing it (validateCustomProvider,mergeFetchedModels), with no precedent for mocking the SDK + global sync in a component, and that pure extraction is already done.@ai-sdk/openai-compatible.Risk Notes
config.provider.<id>.models, which marks the providersource: "config"indatabase, but the later api-key pass (provider.ts:1382) overwritessourceback to"api"for any provider with a stored API key. So a key-connected provider like Kilo keeps its "API key" tag in Settings > Providers. The written config entry carries nonpm, soisConfigCustomstays false and the disconnect path is unchanged.How To Verify
Screenshots or Recordings
Visual check via the added snap target
settings-models-fetch(bun run snap settings-models-fetch; grid PNG underdocs/design/preview/screenshots/). It captures the real Settings > Models provider group with the Fetch models button (in-houserefreshicon, right-aligned in the group header) above the model rows with their visibility toggles.Checklist
bug,enhancement,task,documentation.app,ui,platform,harness,ci.P0,P1,P2,P3.Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit