Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ bun run dev:gui
| **Add provider** | Search registry-backed presets for account login, API-key services, local servers, or a custom endpoint. |
| **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

| **Logs** | Auto-refresh recent requests with tokens, requested effort, resolved model, provider, status, request id, duration, and error details. |
| **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. |
| **Stop** | Gracefully stop the proxy and installed background service, restore native Codex, and exit (`POST /api/stop`). |
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ export const de = {
"models.noRoutedHint": "Melde dich zuerst bei einem Anbieter an oder füge einen hinzu.",
"models.emptyDiscovery": "Es wurden keine Modelle gefunden. Prüfe den Anbieter-Endpunkt oder füge ein statisches/eigenes Modell hinzu.",
"models.emptyDiscoveryDisabled": "Die Live-Modellerkennung ist aus und es sind keine statischen Modelle konfiguriert.",
"models.emptyDiscoveryFailed": "{reason}. Der Anbieter bleibt sichtbar — füge ein eigenes Modell hinzu oder korrigiere die Authentifizierung und aktualisiere.",
"models.discoveryFailed": "Erkennung fehlgeschlagen",
"models.discoveryFailedHttp": "Erkennung fehlgeschlagen (HTTP {status})",
"models.discoveryFailedNetwork": "Erkennung fehlgeschlagen (Netzwerk)",
"models.discoveryFailedPolicy": "Erkennung fehlgeschlagen (blockiert)",
"models.discoveryFailedMalformed": "Erkennung fehlgeschlagen (ungültige Antwort)",
"models.openProviderSettings": "Anbietereinstellungen öffnen",
"models.loading": "Lädt…",
"models.search": "Modelle suchen…",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ export const en = {
"models.noRoutedHint": "Log into a provider or add one first.",
"models.emptyDiscovery": "No models were discovered. Check the provider endpoint or add a static/custom model.",
"models.emptyDiscoveryDisabled": "Live model discovery is off and no static models are configured.",
"models.emptyDiscoveryFailed": "{reason}. The provider stays visible — add a custom model or fix auth, then refresh.",
"models.discoveryFailed": "Discovery failed",
"models.discoveryFailedHttp": "Discovery failed (HTTP {status})",
"models.discoveryFailedNetwork": "Discovery failed (network)",
"models.discoveryFailedPolicy": "Discovery failed (blocked)",
"models.discoveryFailedMalformed": "Discovery failed (invalid response)",
"models.openProviderSettings": "Open provider settings",
"models.loading": "Loading…",
"models.search": "Search models…",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ export const ja: Record<TKey, string> = {
"models.noRoutedHint": "まずプロバイダーにログインするか追加してください。",
"models.emptyDiscovery": "モデルが見つかりませんでした。プロバイダーのエンドポイントを確認するか、静的/カスタムモデルを追加してください。",
"models.emptyDiscoveryDisabled": "ライブモデル検出がオフで、静的モデルも設定されていません。",
"models.emptyDiscoveryFailed": "{reason}。プロバイダーは表示されたままです — カスタムモデルを追加するか認証を直してから更新してください。",
"models.discoveryFailed": "検出に失敗しました",
"models.discoveryFailedHttp": "検出に失敗しました (HTTP {status})",
"models.discoveryFailedNetwork": "検出に失敗しました (ネットワーク)",
"models.discoveryFailedPolicy": "検出に失敗しました (ブロック)",
"models.discoveryFailedMalformed": "検出に失敗しました (無効な応答)",
"models.openProviderSettings": "プロバイダー設定を開く",
"models.loading": "読み込み中…",
"models.search": "モデルを検索…",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ export const ko: Record<TKey, string> = {
"models.noRoutedHint": "먼저 프로바이더에 로그인하거나 추가하세요.",
"models.emptyDiscovery": "발견된 모델이 없습니다. 프로바이더 엔드포인트를 확인하거나 정적/사용자 모델을 추가하세요.",
"models.emptyDiscoveryDisabled": "실시간 모델 검색이 꺼져 있고 정적 모델도 설정되지 않았습니다.",
"models.emptyDiscoveryFailed": "{reason}. 프로바이더는 계속 표시됩니다 — 사용자 모델을 추가하거나 인증을 고친 뒤 새로고침하세요.",
"models.discoveryFailed": "모델 검색 실패",
"models.discoveryFailedHttp": "모델 검색 실패 (HTTP {status})",
"models.discoveryFailedNetwork": "모델 검색 실패 (네트워크)",
"models.discoveryFailedPolicy": "모델 검색 실패 (차단됨)",
"models.discoveryFailedMalformed": "모델 검색 실패 (잘못된 응답)",
"models.openProviderSettings": "프로바이더 설정 열기",
"models.loading": "불러오는 중…",
"models.search": "모델 검색…",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ export const ru: Record<TKey, string> = {
"models.noRoutedHint": "Сначала войдите в провайдера или добавьте нового.",
"models.emptyDiscovery": "Модели не обнаружены. Проверьте адрес провайдера или добавьте статическую/пользовательскую модель.",
"models.emptyDiscoveryDisabled": "Автообнаружение моделей выключено, статические модели не настроены.",
"models.emptyDiscoveryFailed": "{reason}. Провайдер остаётся видимым — добавьте пользовательскую модель или исправьте авторизацию и обновите.",
"models.discoveryFailed": "Обнаружение не удалось",
"models.discoveryFailedHttp": "Обнаружение не удалось (HTTP {status})",
"models.discoveryFailedNetwork": "Обнаружение не удалось (сеть)",
"models.discoveryFailedPolicy": "Обнаружение не удалось (заблокировано)",
"models.discoveryFailedMalformed": "Обнаружение не удалось (некорректный ответ)",
"models.openProviderSettings": "Открыть настройки провайдера",
"models.loading": "Загрузка…",
"models.search": "Поиск моделей…",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ export const zh: Record<TKey, string> = {
"models.noRoutedHint": "请先登录提供方或添加一个。",
"models.emptyDiscovery": "未发现任何模型。请检查提供方端点,或添加静态/自定义模型。",
"models.emptyDiscoveryDisabled": "实时模型发现已关闭,且尚未配置静态模型。",
"models.emptyDiscoveryFailed": "{reason}。提供方仍会显示 — 请添加自定义模型或修复鉴权后刷新。",
"models.discoveryFailed": "模型发现失败",
"models.discoveryFailedHttp": "模型发现失败(HTTP {status})",
"models.discoveryFailedNetwork": "模型发现失败(网络)",
"models.discoveryFailedPolicy": "模型发现失败(已拦截)",
"models.discoveryFailedMalformed": "模型发现失败(响应无效)",
"models.openProviderSettings": "打开提供方设置",
"models.loading": "加载中…",
"models.search": "搜索模型…",
Expand Down
30 changes: 30 additions & 0 deletions gui/src/models-groups.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
/** Pure grouping for the Models page, including configured providers with zero model rows. */
import type { TKey } from "./i18n/shared";

/** Mirrors the management-API discovery DTO from `publicModelsDiscoveryStatus`. */
export interface ProviderDiscoverySummary {
ok: boolean;
kind: string;
at?: number;
httpStatus?: number;
fallback?: string;
detail?: string;
}

export interface ConfiguredProviderSummary {
name: string;
authMode?: string;
disabled?: boolean;
liveModels?: boolean;
models?: string[];
discovery?: ProviderDiscoverySummary | null;
}

export interface ProviderModelGroup<Row> {
Expand All @@ -13,6 +26,7 @@ export interface ProviderModelGroup<Row> {
native: boolean;
liveModels: boolean;
configuredModels: string[];
discovery: ProviderDiscoverySummary | null;
}

export function buildProviderModelGroups<Row extends { provider: string; native?: boolean }>(
Expand Down Expand Up @@ -45,10 +59,26 @@ export function buildProviderModelGroups<Row extends { provider: string; native?
native: providerRows.length > 0 && providerRows.every(row => row.native === true),
liveModels: configured?.liveModels !== false,
configuredModels: configured?.models ?? [],
discovery: configured?.discovery ?? null,
};
})
.sort((a, b) => {
if (a.native !== b.native) return a.native ? -1 : 1;
return a.provider.localeCompare(b.provider);
});
}

/** Short Models-page badge label for a failed discovery outcome. */
export function discoveryFailureBadgeLabel(
discovery: ProviderDiscoverySummary | null | undefined,
t: (key: TKey, vars?: Record<string, string | number>) => string,
): string | null {
if (!discovery || discovery.ok) return null;
if (discovery.kind === "http" && typeof discovery.httpStatus === "number") {
return t("models.discoveryFailedHttp", { status: discovery.httpStatus });
}
if (discovery.kind === "network") return t("models.discoveryFailedNetwork");
if (discovery.kind === "policy") return t("models.discoveryFailedPolicy");
if (discovery.kind === "malformed") return t("models.discoveryFailedMalformed");
return t("models.discoveryFailed");
}
28 changes: 23 additions & 5 deletions gui/src/pages/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useT } from "../i18n/shared";
import type { TFn, TKey } from "../i18n/shared";
import { modelLabel } from "../model-display";
import { type ComboItem, parseComboList } from "../combo-workspace-data";
import { buildProviderModelGroups, type ConfiguredProviderSummary } from "../models-groups";
import { buildProviderModelGroups, discoveryFailureBadgeLabel, type ConfiguredProviderSummary } from "../models-groups";

interface ModelRow {
provider: string;
Expand Down Expand Up @@ -726,7 +726,7 @@ export default function Models({ apiBase }: { apiBase: string }) {

{
// eslint-disable-next-line react-hooks/refs -- The hover ref is only read by row event handlers nested in this renderer.
groups.map(({ provider, rows, native: isNative, liveModels }) => {
groups.map(({ provider, rows, native: isNative, liveModels, discovery }) => {
const isCollapsed = collapsed.has(provider);
const activeCount = rows.filter(m => !disabled.has(m.namespaced)).length;
const capOn = contextCaps[provider] === contextCapValue;
Expand All @@ -742,6 +742,7 @@ export default function Models({ apiBase }: { apiBase: string }) {
const remaining = filtered.length - visible.length;
const allOn = rows.length > 0 && rows.every(m => !disabled.has(m.namespaced));
const allOff = rows.length > 0 && rows.every(m => disabled.has(m.namespaced));
const discoveryBadge = discoveryFailureBadgeLabel(discovery, t);
const bulkToggle = (enable: boolean) => {
const next = new Set(disabled);
for (const m of rows) { if (enable) next.delete(m.namespaced); else next.add(m.namespaced); }
Expand All @@ -754,6 +755,14 @@ export default function Models({ apiBase }: { apiBase: string }) {
<IconChevron style={{ width: 14, height: 14, color: "var(--muted)", transform: isCollapsed ? "none" : "rotate(90deg)", transition: "transform .12s" }} />
<span className="text-body font-semibold">{provider}</span>
{isNative && <span className="muted mono text-caption" style={{ padding: "1px 6px", border: "1px solid var(--border)", borderRadius: "var(--radius-pill)" }}>{t("models.nativeGroupLabel")}</span>}
{discoveryBadge && (
<span
className="badge badge-amber"
title={discovery?.detail ? `${discoveryBadge}: ${discovery.detail}` : discoveryBadge}
>
{discoveryBadge}
</span>
)}
<span className="muted mono text-label">{t("models.active", { active: activeCount, total: rows.length })}</span>
<div style={{ flex: 1 }} />
<div className="row" onClick={e => e.stopPropagation()} style={{ gap: 6 }}>
Expand Down Expand Up @@ -792,7 +801,7 @@ export default function Models({ apiBase }: { apiBase: string }) {
{!isCollapsed && (
<div style={{ padding: "6px 12px" }}>
{isNative && <p className="muted text-label" style={{ margin: "2px 0 6px" }}>{t("models.nativeHint")}</p>}
{rows.length === 0 && <EmptyProviderHint liveModels={liveModels} />}
{rows.length === 0 && <EmptyProviderHint liveModels={liveModels} discovery={discovery} />}
{rows.length > PAGE / 2 && (
<input
className="input"
Expand Down Expand Up @@ -1108,13 +1117,22 @@ export default function Models({ apiBase }: { apiBase: string }) {
);
}

export function EmptyProviderHint({ liveModels }: { liveModels: boolean }) {
export function EmptyProviderHint({
liveModels,
discovery,
}: {
liveModels: boolean;
discovery?: ConfiguredProviderSummary["discovery"];
}) {
const t = useT();
const discoveryBadge = discoveryFailureBadgeLabel(discovery, t);
return (
<div className="row muted text-label leading-body" role="status" style={{ alignItems: "flex-start", gap: 8, padding: "6px 0" }}>
<IconInfo width={15} height={15} aria-hidden="true" style={{ flexShrink: 0, marginTop: 2 }} />
<span>
{t(liveModels ? "models.emptyDiscovery" : "models.emptyDiscoveryDisabled")}{" "}
{discoveryBadge
? <>{t("models.emptyDiscoveryFailed", { reason: discoveryBadge })}{" "}</>
: <>{t(liveModels ? "models.emptyDiscovery" : "models.emptyDiscoveryDisabled")}{" "}</>}
<a href="#providers">{t("models.openProviderSettings")}</a>
</span>
</div>
Expand Down
17 changes: 15 additions & 2 deletions gui/tests/models-empty-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { LanguageProvider } from "../src/i18n/provider";
import { EmptyProviderHint } from "../src/pages/Models";
import type { ProviderDiscoverySummary } from "../src/models-groups";

let previousLanguage: unknown;

Expand All @@ -20,10 +21,10 @@ afterEach(() => {
});
});

function renderHint(liveModels: boolean): string {
function renderHint(liveModels: boolean, discovery?: ProviderDiscoverySummary | null): string {
return renderToStaticMarkup(
<LanguageProvider>
<EmptyProviderHint liveModels={liveModels} />
<EmptyProviderHint liveModels={liveModels} discovery={discovery} />
</LanguageProvider>,
);
}
Expand All @@ -40,3 +41,15 @@ test("empty static provider explains that live discovery is disabled", () => {
expect(html).toContain("Live model discovery is off");
expect(html).toContain('role="status"');
});

test("empty provider with HTTP 401 discovery status surfaces the concrete reason (#329)", () => {
const html = renderHint(true, {
ok: false,
kind: "http",
httpStatus: 401,
fallback: "configured",
});
expect(html).toContain("Discovery failed (HTTP 401)");
expect(html).toContain("provider stays visible");
expect(html).toContain('href="#providers"');
});
Loading
Loading