Skip to content

feat(gui): add provider model loader#407

Closed
HaydernCenterpoint wants to merge 4 commits into
lidge-jun:devfrom
HaydernCenterpoint:feat/provider-model-loader
Closed

feat(gui): add provider model loader#407
HaydernCenterpoint wants to merge 4 commits into
lidge-jun:devfrom
HaydernCenterpoint:feat/provider-model-loader

Conversation

@HaydernCenterpoint

@HaydernCenterpoint HaydernCenterpoint commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add provider catalog and model selection UI
  • wire live model discovery into Add Provider
  • keep provider discovery race-safe

Dependencies

Merge after #405 and #406. Their commits disappear from this diff as prerequisites land.

Tests

  • provider catalog and discovery race tests

Summary by CodeRabbit

  • New Features
    • Added a provider directory with search, access categories, verification badges, documentation links, and responsive browsing.
    • Added live or static model discovery, allowing selection of models to configure as defaults.
    • Added support for regional endpoints, including China mainland and international options.
    • Added provider icons and expanded catalog coverage, including directory-only providers.
  • Bug Fixes
    • Improved model-list handling and fallback behavior when provider discovery fails.
    • Prevented outdated discovery results from replacing current provider selections.
  • Localization
    • Added translations for the new provider catalog and setup flows.

Copilot AI review requested due to automatic review settings July 24, 2026 11:15

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94f2c427-e65f-4cba-a43a-0f68dee02126

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a free-provider directory with access metadata, trusted live/static model discovery, normalized model-list parsing, and a redesigned provider catalog and setup modal with model selection, cancellation, responsive layouts, icons, and localized strings.

Changes

Provider directory and registry

Layer / File(s) Summary
Directory metadata and registry projection
src/providers/free-directory.ts, src/providers/registry.ts, src/providers/derive.ts, gui/src/components/provider-catalog/provider-presets.ts
Defines free-provider access groups and metadata, merges directory-only providers into the registry, projects governance fields into presets, and adds catalog grouping/actionability helpers.
Directory and registry validation
gui/tests/provider-catalog.test.ts, tests/provider-free-directory.test.ts, tests/provider-registry-parity.test.ts
Validates directory counts, group membership, registry parity, derived presets, trusted model URLs, and optional-key provider mappings.

Model discovery backend

Layer / File(s) Summary
Trusted discovery and model normalization
src/server/management/provider-routes.ts, src/codex/catalog/provider-fetch.ts, src/oauth/index.ts
Adds the provider-preset discovery endpoint, validates trusted base URLs, supports static/live catalogs, normalizes model responses, rejects redirects, and prefers registry model URLs.
Discovery and routing coverage
tests/provider-free-directory.test.ts, tests/provider-model-envelope.test.ts, tests/google-models-listing.test.ts, tests/provider-directory-routing.test.ts
Covers response normalization, live/static fallback, endpoint restrictions, reference providers, Google envelopes, persistence, and directory-only routing.

Provider catalog UI

Layer / File(s) Summary
Catalog browsing and setup flow
gui/src/components/provider-catalog/ProviderCatalog.tsx, gui/src/components/AddProviderModal.tsx
Adds grouped catalog browsing, searchable account rows, provider details, actionable/reference setup states, model discovery, selected-model submission, and stale-request cancellation.
Presentation and localization
gui/src/styles/provider-catalog.css, gui/src/provider-icons.ts, gui/src/i18n/*.ts, gui/tests/provider-discovery-race.test.tsx
Adds catalog/detail responsive styling, provider icon aliases, localized catalog and discovery strings, and race-safety coverage for changing providers during discovery.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • lidge-jun/opencodex#405: Extends the shared provider preset model and registry/derive metadata used by this directory implementation.

Suggested reviewers: lidge-jun, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 is concise and matches the main GUI change: adding provider model discovery/loading UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60046f85af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +163 to +167
const invalidateDiscoveryRequest = () => {
discoveryRequestRef.current += 1;
discoveryAbortRef.current?.abort();
discoveryAbortRef.current = null;
setDiscoveryBusy(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear discovered models when inputs are invalidated

When a user runs Discover models and then changes the endpoint/base URL/API key/adapter, all of those handlers call invalidateDiscoveryRequest(), but this helper only aborts pending fetches and leaves discoveredModels, selectedModels, and discoverySource intact. The stale list remains visible and submit() can persist that old selectedModels allowlist for the new endpoint without another discovery, so models from one regional/provider endpoint can be saved against a different one. Clear the discovered model state whenever invalidating due to provider input changes.

Useful? React with 👍 / 👎.

Comment on lines +394 to +396
const allowsModelsEnvelope = (prov.adapter === "google" && (prov.googleMode ?? "ai-studio") === "ai-studio")
|| new URL(url).pathname.replace(/\/+$/, "").endsWith("/api/tags");
const items = parseProviderModelsApiItems(json, allowsModelsEnvelope);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter Google discovery to generative models

For Google AI Studio, the /v1beta/models envelope includes resources whose supported methods are embedding-only, but this new path accepts every name and exposes it as a chat model. The Google adapter later calls .../models/${id}:generateContent, so selecting an embedding-only id such as text-embedding-004 from live discovery saves a model that fails every chat request. Filter Google rows to models supporting generateContent/stream before adding them to the catalog or discovery response.

Useful? React with 👍 / 👎.

Comment on lines +449 to +450
const provider = {
...providerConfigSeed(entry),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor adapter overrides during discovery

If a user changes the visible Adapter field before clicking Discover models, this route still rebuilds discovery from providerConfigSeed(entry) and never copies supplied.adapter, while the later /api/providers save path does persist the user's edited adapter. That lets discovery succeed against the preset transport (for example Google) and then save the same selected/default models against a different transport (for example openai-chat), where subsequent chat requests fail. Either reject adapter overrides for presets in the discover flow or run discovery with the same adapter that will be saved.

Useful? React with 👍 / 👎.

const { resolveModelsAuthToken, buildModelsRequest } = await import("../../oauth");
const apiKey = await resolveModelsAuthToken(presetId, provider);
const request = buildModelsRequest(provider, apiKey, presetId);
const modelsDestinationError = await providerDestinationResolvedError(presetId, { baseUrl: request.url });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve private-network opt-in for model URLs

For presets that allow custom endpoints, such as Qwen Cloud, a user can enter a local/private compatible endpoint and check allowPrivateNetwork; the first destination check accepts that provider, but the second check against the derived /models URL drops allowPrivateNetwork. Discovery is therefore rejected even though the same provider can be saved and routed with the opt-in. Pass the provider's allowPrivateNetwork through this models-URL check as well.

Useful? React with 👍 / 👎.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/management/provider-routes.ts (1)

268-323: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

/api/providers/test still allows fetch redirects — inconsistent with the SSRF hardening this PR just added.

This PR adds redirect: "error" to the discover endpoint's fetch (line 476) and to fetchProviderModels (provider-fetch.ts:368) specifically to stop a redirect from bypassing the destination-policy (SSRF) check performed on the pre-redirect URL. The connectivity-test endpoint at Line 295 does the exact same thing — build a /models request from a provider's baseUrl via buildModelsRequest and fetch it with the caller-supplied credential — but keeps the default redirect: "follow" and never re-validates the destination at request time. A provider's baseUrl is destination-checked only when it's saved (POST/PATCH /api/providers); a DNS-rebind or a malicious upstream 3xx at test time can still redirect this fetch into a private/metadata address.

🛡️ Suggested fix
     const { url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
     const started = Date.now();
     try {
-      const res = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });
+      const res = await fetch(modelsUrl, { headers, redirect: "error", signal: AbortSignal.timeout(8000) });

As per path instructions for src/**: "Flag ... changes that bypass the shared routing/config layers" — this endpoint bypasses the same providerDestinationResolvedError re-check the sibling discover/fetch paths now perform.

🤖 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 `@src/server/management/provider-routes.ts` around lines 268 - 323, Update the
fetch call in the /api/providers/test handler to use redirect: "error", matching
the SSRF-hardening behavior of the sibling provider-fetch and discover paths.
Preserve the existing timeout, headers, response handling, and error response
behavior while preventing upstream redirects from being followed.

Source: Path instructions

🤖 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 `@gui/src/i18n/de.ts`:
- Around line 1002-1036: Update the German locale export to import the TKey type
from en.ts and type its translations as Record<TKey, string>, removing the local
TKey = keyof typeof de definition. Preserve the existing German translation
entries while ensuring missing or extra keys are checked against the English
locale key set.

In `@gui/src/styles/provider-catalog.css`:
- Around line 12-19: Define a shared --catalog-head-h custom property for the
modal header height, set it to 62px in the default styles and 56px in the mobile
media query, then reuse it in both .provider-loader-card > .modal-head
min-height and .provider-catalog height calculations. Remove the duplicated
literal height values while preserving the existing responsive behavior.

---

Outside diff comments:
In `@src/server/management/provider-routes.ts`:
- Around line 268-323: Update the fetch call in the /api/providers/test handler
to use redirect: "error", matching the SSRF-hardening behavior of the sibling
provider-fetch and discover paths. Preserve the existing timeout, headers,
response handling, and error response behavior while preventing upstream
redirects from being followed.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 74d4f98e-5638-440a-aa76-14ec2794b759

📥 Commits

Reviewing files that changed from the base of the PR and between cc7bb57 and 60046f8.

📒 Files selected for processing (24)
  • gui/src/components/AddProviderModal.tsx
  • gui/src/components/provider-catalog/ProviderCatalog.tsx
  • gui/src/components/provider-catalog/provider-presets.ts
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/provider-icons.ts
  • gui/src/styles/provider-catalog.css
  • gui/tests/provider-catalog.test.ts
  • gui/tests/provider-discovery-race.test.tsx
  • src/codex/catalog/provider-fetch.ts
  • src/oauth/index.ts
  • src/providers/derive.ts
  • src/providers/free-directory.ts
  • src/providers/registry.ts
  • src/server/management/provider-routes.ts
  • tests/google-models-listing.test.ts
  • tests/provider-directory-routing.test.ts
  • tests/provider-free-directory.test.ts
  • tests/provider-model-envelope.test.ts
  • tests/provider-registry-parity.test.ts

Comment thread gui/src/i18n/de.ts
Comment on lines +1002 to +1036
"modal.catalogModes": "Zugriffsarten der Anbieter",
"modal.accessGroups": "Kostenlose Zugriffsgruppen",
"modal.providerDirectory": "Anbieterverzeichnis",
"modal.searchCount": "{count} Anbieter durchsuchen",
"modal.group.all": "Alle Anbieter",
"modal.group.recurring-or-keyless": "Regelmäßig / ohne Schlüssel",
"modal.group.recurring-uncapped": "Kein öffentliches Token-Limit",
"modal.group.recurring-credit": "Regelmäßiges Guthaben",
"modal.group.signup-credit": "Startguthaben",
"modal.group.existing": "Vorhandene kostenlose & lokale",
"modal.paidHint": "Kostenpflichtige Anbieter und Anbieter mit einmaligem Startguthaben durchsuchen.",
"modal.selectProvider": "Anbieter auswählen",
"modal.selectProviderHint": "Wähle einen Anbieter, um die Einrichtung anzusehen und Modelle zu suchen.",
"modal.badge.recurring": "Regelmäßig kostenlos",
"modal.badge.reference": "Referenz",
"modal.badge.experimental": "Experimentell",
"modal.badge.supported": "Unterstützt",
"modal.verification.official": "Offizielle Dokumentation",
"modal.verification.primary": "Primärquelle",
"modal.verification.unverified": "Nicht verifiziert",
"modal.providerDocs": "Anbieterdokumentation",
"modal.referenceExplanation": "Dieser Eintrag dient nur als Referenz. Automatische Aufrufe und die Modellsuche bleiben deaktiviert, bis ein dokumentierter Transportweg verifiziert wurde.",
"modal.modelDiscovery": "Modellsuche",
"modal.modelDiscoveryHint": "Verfügbare Modelle für diese Einrichtung suchen.",
"modal.discoverModels": "Modelle suchen",
"modal.discovering": "Suche läuft…",
"modal.modelsLive": "Live-Antwort des Anbieters",
"modal.modelsStatic": "Statischer Katalog",
"modal.discoveredModels": "Gefundene Modelle",
"modal.discoveryFailed": "Für diese Einrichtung konnten keine Modelle gefunden werden.",
"modal.backToDirectory": "Zurück zum Anbieterverzeichnis",
"modal.setupReviewProvider": "Einrichtungsdokumentation für {label} prüfen.",
"modal.setupReferenceStep": "Nutze diesen Eintrag als Einrichtungshilfe, bis verifizierte Aufrufunterstützung verfügbar ist.",
"modal.setupKeylessStep": "Bestätige den Endpunkt unten; ein API-Schlüssel ist optional.",
"modal.setupDiscoverStep": "Modelle suchen und ein Standardmodell auswählen.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

German locale is the only one not type-checked against en.ts's key set.

All 37 new keys are present and consistent here (verified against ja/ko/ru/zh), so nothing is broken today. But de.ts exports its own TKey = keyof typeof de (line 1163) instead of import type { TKey } from "./en" + Record<TKey, string> like every other locale file. That means a future key added to en.ts but missed in de.ts won't be caught at compile time here, unlike ja/ko/ru/zh.

♻️ Suggested fix
-export const de = {
+import type { TKey } from "./en";
+
+export const de: Record<TKey, string> = {
   "nav.dashboard": "Übersicht",
   ...
-} as const;
-
-export type TKey = keyof typeof de;
+};
🤖 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 `@gui/src/i18n/de.ts` around lines 1002 - 1036, Update the German locale export
to import the TKey type from en.ts and type its translations as Record<TKey,
string>, removing the local TKey = keyof typeof de definition. Preserve the
existing German translation entries while ensuring missing or extra keys are
checked against the English locale key set.

Comment on lines +12 to +19
.provider-loader-card > .modal-head {
min-height: 62px;
margin: 0;
padding: 12px 18px;
border-bottom: 1px solid var(--border);
}

.provider-catalog { display: flex; flex-direction: column; min-height: 0; height: calc(100% - 62px); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Modal-head height (62px/56px) is duplicated as a magic number across two rules per breakpoint.

.provider-loader-card > .modal-head { min-height: 62px; ... } / .provider-catalog { height: calc(100% - 62px); } (and the 56px mobile pair at lines 102-103) must be kept in sync by hand; a future tweak to one is easy to forget in the other, silently breaking the catalog's internal scroll height.

Consider a CSS custom property, e.g. --catalog-head-h: 62px; (56px in the mobile media query), referenced from both rules.

Also applies to: 100-103

🤖 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 `@gui/src/styles/provider-catalog.css` around lines 12 - 19, Define a shared
--catalog-head-h custom property for the modal header height, set it to 62px in
the default styles and 56px in the mobile media query, then reuse it in both
.provider-loader-card > .modal-head min-height and .provider-catalog height
calculations. Remove the duplicated literal height values while preserving the
existing responsive behavior.

@Wibias
Wibias marked this pull request as draft July 25, 2026 01:20
@HaydernCenterpoint
HaydernCenterpoint force-pushed the feat/provider-model-loader branch from 60046f8 to 9638108 Compare July 25, 2026 02:05
@HaydernCenterpoint

Copy link
Copy Markdown
Contributor Author

Closed in favor of the consolidated provider upgrade PR, which combines the work from #406 through #413 into one reviewed branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants