Skip to content

fix(models): surface discovery failure status when /v1/models returns 401 (#329)#354

Closed
stantheman0128 wants to merge 1 commit into
lidge-jun:devfrom
stantheman0128:fix/329-provider-401-vanish
Closed

fix(models): surface discovery failure status when /v1/models returns 401 (#329)#354
stantheman0128 wants to merge 1 commit into
lidge-jun:devfrom
stantheman0128:fix/329-provider-401-vanish

Conversation

@stantheman0128

@stantheman0128 stantheman0128 commented Jul 23, 2026

Copy link
Copy Markdown

Summary

AI assistance

Drafted and verified with Cursor/Grok assistance. Human (Stan Shih / stantheman0128) reviewed the approach, Evidence, and PR text before opening.

Verification

bun test tests/provider-discovery-status.test.ts tests/provider-discovery-status-api.test.ts tests/models-page-groups.test.ts
# 12 pass, 0 fail

cd gui && bun test tests/models-empty-provider.test.tsx
# 3 pass, 0 fail (includes HTTP 401 empty-hint SSR)

bun test tests/codex-catalog.test.ts -t "failed discovery|defaultModel|successful live"
# 3 pass (includes #308 defaultModel fallback)

bun node_modules/typescript/bin/tsc --noEmit
# exit 0

cd gui && bun run lint
# exit 0

Evidence (product path)

Started a loopback upstream that returns HTTP 401 on /models while accepting POSTs, configured an anthropic-compatible agent-plan provider with defaultModel: glm-5-2 and no static models[], then hit the same management endpoints the Models page uses:

GET /api/models
# agent-plan/glm-5-2 still present (provider does not vanish)

GET /api/providers
# agent-plan.discovery = { ok:false, kind:"http", httpStatus:401, fallback:"configured" }

After clearModelCache("agent-plan") (same path provider PATCH / key updates use), discovery is null again so a retry is not stuck on a stale 401 badge.

What was not tested

  • Live Volcengine Ark Agent Plan credentials (no key in this environment). The product path above uses a local Bun.serve mock with the same 401 listing / POST-ok shape.
  • Full browser click-through screenshot of the Models tab; GUI coverage is SSR of EmptyProviderHint plus grouping/badge unit tests against the same discovery DTO the page consumes.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features

    • Added clearer model discovery status in the Models page.
    • Provider groups remain visible when discovery is disabled, empty, or fails.
    • Added amber status badges and detailed tooltips for HTTP, network, policy, and malformed-response failures.
    • Configured fallback or custom models remain available after discovery failures.
    • Added localized discovery failure messages across supported languages.
  • Documentation

    • Expanded dashboard documentation to describe model discovery visibility and failure behavior.
  • Tests

    • Added coverage for discovery failures, fallback models, status badges, and recovery after cache clearing.

Keep providers visible after /v1/models auth failures and expose the last
discovery outcome (HTTP 401/403, network, malformed) so the Models tab can
badge the group and guide manual custom-model entry (lidge-jun#329).

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added the bug Something isn't working label Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Live provider model discovery now records categorized outcomes and fallback details, exposes them through /api/providers, and displays localized failure badges or empty states while retaining configured models and provider visibility.

Changes

Provider discovery status

Layer / File(s) Summary
Discovery status storage and contracts
src/codex/model-cache.ts
Adds per-provider discovery status types, storage accessors, public DTO conversion, and cache invalidation.
Discovery outcome recording
src/codex/catalog/provider-fetch.ts, tests/provider-discovery-status.test.ts
Records skipped, successful, empty, policy, HTTP, malformed, and network outcomes while preserving configured or stale fallback models; tests cover failures, recovery, cooldowns, and default-model-only providers.
Provider API propagation
src/server/management/provider-routes.ts, tests/provider-discovery-status-api.test.ts
Adds discovery metadata to /api/providers and verifies HTTP 401 visibility, configured fallback models, and cache clearing.
Models UI and localized messaging
gui/src/models-groups.ts, gui/src/pages/Models.tsx, gui/src/i18n/*.ts, gui/tests/*, tests/models-page-groups.test.ts, docs-site/src/content/docs/guides/web-dashboard.md
Threads discovery metadata into provider groups, renders amber failure badges and failure-specific empty states, adds translations, tests label formatting and HTTP 401 rendering, and documents the behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant ModelsPage
  participant ProvidersAPI
  participant ProviderModelFetcher
  participant ModelCache
  ModelsPage->>ProvidersAPI: load provider summaries
  ProviderModelFetcher->>ModelCache: record discovery outcome
  ProvidersAPI->>ModelCache: read public discovery status
  ModelCache-->>ProvidersAPI: status, fallback, and detail
  ProvidersAPI-->>ModelsPage: provider with discovery metadata
  ModelsPage->>ModelsPage: render badge or empty-state message
Loading

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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 matches the main change: surfacing discovery failure status for 401 model-list failures in Models.
Linked Issues check ✅ Passed The PR keeps providers visible on discovery failure, adds failure badges/status to the API/UI, and preserves configured/custom models as required by #329.
Out of Scope Changes check ✅ Passed Changes stay focused on discovery-status plumbing, UI rendering, translations, and tests tied to #329.
✨ 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: 9f312d1f9e

ℹ️ 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".

codexAccountMode: providerCodexAccountMode(name, p),
// Last live /models outcome (null until the first attempt). Keeps HTTP 401 / network
// failures visible on the Models page without re-fetching during cooldown (#329).
discovery: publicModelsDiscoveryStatus(name),

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 Return discovery after the catalog poll finishes

When the Models page first loads a provider with a slow or failing live /models endpoint, it requests /api/models and /api/providers in parallel; this line snapshots the in-memory status before /api/models has finished gatherRoutedModels() and called setModelsDiscoveryStatus, so the initial provider payload can contain discovery: null and the new HTTP 401/network badge is missing until the 10s poll or a manual refresh. Consider having the GUI fetch providers after the models request completes, or make this endpoint await/trigger the same catalog poll before returning discovery.

Useful? React with 👍 / 👎.

Comment thread src/codex/model-cache.ts
Comment on lines +89 to +90
export function publicModelsDiscoveryStatus(provider: string): ProviderDiscoveryStatus | null {
return getModelsDiscoveryStatus(provider) ?? null;

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 discovery when OAuth credentials change

This public status is keyed only by provider name, but the OAuth credential-change paths (/api/oauth/logout, /api/oauth/accounts/active, and account removal/reauth) clear login/quota state without calling clearModelCache, and fetchProviderModels returns early for an OAuth provider with no token without overwriting discovery. After a recorded 401/network failure, logging out or switching/reauthing the account can therefore leave /api/providers returning the old badge until a later successful live discovery or restart; with no token it can persist indefinitely. Clear the model cache/discovery status in those OAuth paths before exposing this DTO.

Useful? React with 👍 / 👎.

Comment on lines +279 to +283
setModelsDiscoveryStatus(name, {
ok: false,
kind: "network",
fallback,
detail: liveResult.error,

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 Cursor discovery failure classification

fetchCursorUsableModels already distinguishes auth, http, timeout/transport, decode, and empty, but this new status maps every non-ok Cursor result to kind: "network". For an expired Cursor key (401/403) or a malformed RPC response, the Models page will show “Discovery failed (network)” instead of an auth/HTTP or invalid-response reason, sending users toward the wrong fix. Map Cursor auth/http/decode outcomes to the matching discovery kind/detail instead of hard-coding network.

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: 3

🤖 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 `@docs-site/src/content/docs/guides/web-dashboard.md`:
- Line 39: Update the Models guide rows in the Korean, Russian, and Simplified
Chinese web-dashboard documents to mirror the current English row, including
zero-model groups, the amber discovery-failure badge, and the retained “+”
custom-model action. Preserve each locale’s existing table and translation
formatting while replacing only the outdated Models copy.

In `@src/codex/model-cache.ts`:
- Around line 44-52: Enforce the secret-free contract for
ProviderDiscoveryStatus.detail at the setModelsDiscoveryStatus boundary before
storing or exposing it through
getModelsDiscoveryStatus/publicModelsDiscoveryStatus. Sanitize or reject
arbitrary diagnostic text, covering common credential and sensitive-response
patterns such as Bearer tokens, sk- keys, Authorization headers, raw error
details, and response snippets, while preserving the existing length limit and
public API behavior for approved diagnostics.

In `@tests/provider-discovery-status.test.ts`:
- Around line 21-31: Update the shared configuration helper in
provider-discovery-status tests to include the required OcxConfig fields port
and defaultProvider, then reuse that helper for each gatherRoutedModels setup at
the referenced test sections instead of repeating incomplete configuration
objects.
🪄 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: 16944692-c20d-4137-902c-dd70704a44e8

📥 Commits

Reviewing files that changed from the base of the PR and between 05ee9ec and 9f312d1.

📒 Files selected for processing (16)
  • docs-site/src/content/docs/guides/web-dashboard.md
  • 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/models-groups.ts
  • gui/src/pages/Models.tsx
  • gui/tests/models-empty-provider.test.tsx
  • src/codex/catalog/provider-fetch.ts
  • src/codex/model-cache.ts
  • src/server/management/provider-routes.ts
  • tests/models-page-groups.test.ts
  • tests/provider-discovery-status-api.test.ts
  • tests/provider-discovery-status.test.ts

| **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, and configure quota auto-switch and transient-failure failover. |
| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. |
| **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. |
| **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. Failed live discovery (for example HTTP 401 on `/v1/models`) shows an amber status badge and keeps the provider's `+` custom-model action available. |

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'web-dashboard.md' docs-site/src/content/docs

Repository: lidge-jun/opencodex

Length of output: 427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  docs-site/src/content/docs/guides/web-dashboard.md \
  docs-site/src/content/docs/ja/guides/web-dashboard.md \
  docs-site/src/content/docs/ko/guides/web-dashboard.md \
  docs-site/src/content/docs/ru/guides/web-dashboard.md \
  docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
do
  echo "==== $f ===="
  wc -l "$f"
  # show the Models row area if present
  rg -n -C 2 'Models|zero-model|custom-model|discovery|v1/models|amber|badge' "$f" || true
  echo
done

Repository: lidge-jun/opencodex

Length of output: 4322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  docs-site/src/content/docs/guides/web-dashboard.md \
  docs-site/src/content/docs/ja/guides/web-dashboard.md \
  docs-site/src/content/docs/ko/guides/web-dashboard.md \
  docs-site/src/content/docs/ru/guides/web-dashboard.md \
  docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
do
  echo "==== $f ===="
  wc -l "$f"
  rg -n -C 3 'Models|zero-model|custom-model|discovery|v1/models|amber|badge' "$f" || true
  echo
done

Repository: lidge-jun/opencodex

Length of output: 5234


🏁 Script executed:

#!/bin/bash
pwd

Repository: lidge-jun/opencodex

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  docs-site/src/content/docs/ja/guides/web-dashboard.md \
  docs-site/src/content/docs/ko/guides/web-dashboard.md \
  docs-site/src/content/docs/ru/guides/web-dashboard.md \
  docs-site/src/content/docs/zh-cn/guides/web-dashboard.md
do
  echo "==== $f ===="
  nl -ba "$f" | sed -n '30,45p'
  echo
done

Repository: lidge-jun/opencodex

Length of output: 262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f=docs-site/src/content/docs/ja/guides/web-dashboard.md
echo "==== $f ===="
wc -l "$f"
nl -ba "$f" | sed -n '30,45p'

Repository: lidge-jun/opencodex

Length of output: 319


Update the translated web-dashboard guide rows

docs-site/src/content/docs/ko/guides/web-dashboard.md:39, docs-site/src/content/docs/ru/guides/web-dashboard.md:39, and docs-site/src/content/docs/zh-cn/guides/web-dashboard.md:38 still use the older Models copy. Mirror the docs-site/src/content/docs/guides/web-dashboard.md:39 text so the locale docs also mention zero-model groups, the amber discovery-failure badge, and the retained + custom-model action.

🤖 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 `@docs-site/src/content/docs/guides/web-dashboard.md` at line 39, Update the
Models guide rows in the Korean, Russian, and Simplified Chinese web-dashboard
documents to mirror the current English row, including zero-model groups, the
amber discovery-failure badge, and the retained “+” custom-model action.
Preserve each locale’s existing table and translation formatting while replacing
only the outdated Models copy.

Source: Path instructions

Comment thread src/codex/model-cache.ts
Comment on lines +44 to +52
export interface ProviderDiscoveryStatus {
ok: boolean;
kind: ProviderDiscoveryKind;
at: number;
httpStatus?: number;
fallback?: ProviderDiscoveryFallback;
/** Short, secret-free diagnostic (error name or policy reason). */
detail?: string;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

detail is serialized into a public API response with no sanitization guardrail.

ProviderDiscoveryStatus.detail is documented as "Short, secret-free diagnostic," but setModelsDiscoveryStatus (Line 69-82) only truncates it to 160 chars — it never validates/redacts content. This value flows unmodified through getModelsDiscoveryStatus/publicModelsDiscoveryStatus (Line 84-91) straight into the /api/providers GET response (discovery: publicModelsDiscoveryStatus(name) in provider-routes.ts). If any future caller passes a raw error message, response body snippet, or header value as detail, it would be exposed to every GUI client hitting that endpoint.

Consider enforcing the "secret-free" contract at the boundary rather than relying only on caller discipline, e.g. a small allowlist/regex strip for common secret shapes (Bearer , sk-, Authorization:), or restricting detail to a closed set of known diagnostic strings instead of arbitrary text.

🛡️ Example guard
+const SECRET_PATTERN = /(bearer\s+\S+|sk-[a-zA-Z0-9]{10,}|authorization\s*:\s*\S+)/i;
+
 export function setModelsDiscoveryStatus(
   provider: string,
   status: Omit<ProviderDiscoveryStatus, "at"> & { at?: number },
 ): void {
   const next: ProviderDiscoveryStatus = {
     ok: status.ok,
     kind: status.kind,
     at: status.at ?? Date.now(),
   };
   if (typeof status.httpStatus === "number") next.httpStatus = status.httpStatus;
   if (status.fallback) next.fallback = status.fallback;
-  if (status.detail) next.detail = status.detail.slice(0, 160);
+  if (status.detail) next.detail = status.detail.slice(0, 160).replace(SECRET_PATTERN, "[redacted]");
   discoveryStatus.set(provider, next);
 }

Based on path instructions for src/**: "Watch for credential handling: tokens and OAuth material must never be logged or serialized into responses."

Also applies to: 69-82, 88-91

🤖 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/codex/model-cache.ts` around lines 44 - 52, Enforce the secret-free
contract for ProviderDiscoveryStatus.detail at the setModelsDiscoveryStatus
boundary before storing or exposing it through
getModelsDiscoveryStatus/publicModelsDiscoveryStatus. Sanitize or reject
arbitrary diagnostic text, covering common credential and sensitive-response
patterns such as Bearer tokens, sk- keys, Authorization headers, raw error
details, and response snippets, while preserving the existing length limit and
public API behavior for approved diagnostics.

Source: Path instructions

Comment on lines +21 to +31
const models = await gatherRoutedModels({
providers: {
[provider]: {
baseUrl: "https://example.invalid/api/plan",
adapter: "anthropic",
authMode: "key",
apiKey: "plan-key",
defaultModel: "glm-5-2",
},
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'interface OcxConfig {
  $$$
}' --lang typescript src | head -50
rg -n 'interface OcxConfig' -A 30 src

Repository: lidge-jun/opencodex

Length of output: 5498


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tests/provider-discovery-status.test.ts ==\n'
cat -n tests/provider-discovery-status.test.ts | sed -n '1,260p'

printf '\n== gatherRoutedModels signature ==\n'
rg -n 'function gatherRoutedModels|const gatherRoutedModels|export .*gatherRoutedModels' src tests -A 8 -B 4

printf '\n== nearby OcxConfig construction patterns ==\n'
rg -n '\bdefaultProvider\b|\bport\b' tests src -A 2 -B 2 | sed -n '1,220p'

Repository: lidge-jun/opencodex

Length of output: 25391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the OcxConfig definition and any helper/factory functions that may supply defaults.
sed -n '422,490p' src/types.ts | cat -n

printf '\n== config factories ==\n'
rg -n 'defaultProvider:|port:|providers:' src tests -A 2 -B 2 | sed -n '1,260p'

Repository: lidge-jun/opencodex

Length of output: 21284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether this test file is the only place using a bare { providers } literal.
rg -n 'gatherRoutedModels\(\{\s*providers:' tests src -U -A 8 -B 2

Repository: lidge-jun/opencodex

Length of output: 24266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Resolve the exact gatherRoutedModels parameter type and confirm whether this test file is passing a bare OcxConfig literal.
rg -n 'gatherRoutedModels' src tests -A 6 -B 4

Repository: lidge-jun/opencodex

Length of output: 50375


Add the missing OcxConfig fields

tests/provider-discovery-status.test.ts passes a bare { providers: { ... } } object into gatherRoutedModels, but src/types.ts:422-425 requires port and defaultProvider too. This will fail tsc in CI. Add those fields once in a shared helper and reuse it at lines 21-31, 50-60, 80-90, 111-120, 148-158, and 194-205.

🤖 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 `@tests/provider-discovery-status.test.ts` around lines 21 - 31, Update the
shared configuration helper in provider-discovery-status tests to include the
required OcxConfig fields port and defaultProvider, then reuse that helper for
each gatherRoutedModels setup at the referenced test sections instead of
repeating incomplete configuration objects.

@Wibias

Wibias commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this — we’re landing #329 via #358 (also covers #331) to avoid double-merge conflicts. Planning to carry your skipped / status / docs ideas into that path and credit you there. Closing this PR in favor of #358. (not a rejection of the work).

@Wibias Wibias closed this Jul 23, 2026
@stantheman0128

Copy link
Copy Markdown
Author

Thanks @Wibias / @lidge-jun. Agree with landing #329 via #358 to avoid the double-merge conflict. Glad the discovery-failure badge / status surface is useful; happy to see the skipped + docs follow-ups folded there with credit. Closing in favor of #358 is fine on our side.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants