diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 2c3da6070..f0b905682 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -86,7 +86,7 @@ ocx logout | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude models; live model list fetched from `/v1/models`. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 coding models. | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Initial login imports the installed, signed-in `kiro-cli` session (on Unix, install with `curl -fsSL https://cli.kiro.dev/install | bash`; on Windows PowerShell, use `irm 'https://cli.kiro.dev/install.ps1' | iex`; then run `kiro-cli login`). **Add account** logs `kiro-cli` out, starts a fresh browser login that switches the account used by `kiro-cli`, and stores account-scoped profile metadata. Existing OpenCodex accounts are preserved, and cancellation or failure restores the previous `kiro-cli` session. | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. Uses the maintained six-model static catalog because CCA does not expose the generic `/models` endpoint. | | `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport, and account-filtered model discovery. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. | diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index b47146a0c..634743768 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -83,7 +83,7 @@ ocx logout | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude モデル; ライブモデル一覧は `/v1/models` から取得。 | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 コーディングモデル。 | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 初回ログインは、インストール済みでサインインした `kiro-cli` セッションを取り込みます(Unix では `curl -fsSL https://cli.kiro.dev/install | bash`、Windows PowerShell では `irm 'https://cli.kiro.dev/install.ps1' | iex` でインストールしてから `kiro-cli login` を実行)。**アカウントを追加**は `kiro-cli` をログアウトして新しいブラウザログインを開始し、`kiro-cli` 自体のアカウントを切り替えてアカウント別プロファイルメタデータを保存します。既存の OpenCodex アカウントは保持され、キャンセルまたは失敗時には以前の `kiro-cli` セッションが復元されます。 | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。 | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。CCA は汎用 `/models` エンドポイントを公開しないため、管理された 6 モデルの静的カタログを使用します。 | | `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 実験的。GitHub デバイスフロー + `copilot_internal` 交換(VS Code OAuth クライアント)。有効な Copilot サブスクリプションが必要で、公式のサードパーティ API ではありません。 | diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 968014ce2..891239230 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -83,7 +83,7 @@ ocx logout | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 모델; 실시간 모델 목록은 `/v1/models`에서 가져옵니다. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 코딩 모델. | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 최초 로그인은 설치하고 로그인한 `kiro-cli` 세션을 가져옵니다(Unix에서는 `curl -fsSL https://cli.kiro.dev/install | bash`, Windows PowerShell에서는 `irm 'https://cli.kiro.dev/install.ps1' | iex`로 설치한 뒤 `kiro-cli login` 실행). **계정 추가**는 `kiro-cli`에서 로그아웃한 뒤 새 브라우저 로그인을 시작하여 `kiro-cli` 자체의 계정을 전환하고, 계정별 프로필 메타데이터를 저장합니다. 기존 OpenCodex 계정은 유지되며, 취소되거나 실패하면 이전 `kiro-cli` 세션을 복원합니다. | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. CCA가 범용 `/models` 엔드포인트를 제공하지 않으므로 유지 관리되는 6개 모델 정적 카탈로그를 사용합니다. | | `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 실험적. GitHub 디바이스 플로우 + `copilot_internal` 교환(VS Code OAuth 클라이언트). 활성 Copilot 구독 필요; 공식 서드파티 API가 아닙니다. | diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 8bf6be3bc..d6b18a934 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -89,7 +89,7 @@ ocx logout | `anthropic` | `anthropic` | `https://api.anthropic.com` | Модели Claude; актуальный список моделей загружается из `/v1/models`. | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Модели Kimi K2.7/K2.6/K2.5 для кодинга. | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Первый вход импортирует существующую сессию после установки Kiro CLI (в Unix: `curl -fsSL https://cli.kiro.dev/install | bash`; в Windows PowerShell: `irm 'https://cli.kiro.dev/install.ps1' | iex`; затем выполните `kiro-cli login`). **Добавить аккаунт** выполняет выход из `kiro-cli`, запускает новый вход через браузер, переключает аккаунт самого `kiro-cli` и сохраняет метаданные профиля отдельно для каждого аккаунта. Существующие аккаунты OpenCodex сохраняются; при отмене или сбое восстанавливается предыдущая сессия `kiro-cli`. | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. Используется поддерживаемый статический каталог из шести моделей, поскольку CCA не предоставляет общий эндпоинт `/models`. | | `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. | diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index c86149aa5..522d9e60e 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -77,7 +77,7 @@ ocx logout | `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 模型;实时模型列表从 `/v1/models` 获取。 | | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 编程模型。 | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 首次登录会导入已安装并已登录的 Kiro CLI 会话(Unix 使用 `curl -fsSL https://cli.kiro.dev/install | bash`;Windows PowerShell 使用 `irm 'https://cli.kiro.dev/install.ps1' | iex`;然后运行 `kiro-cli login`)。**添加账户**会先退出 `kiro-cli`,再启动新的浏览器登录,从而切换 `kiro-cli` 自身使用的账户,并保存账户范围的配置文件元数据。现有 OpenCodex 账户会保留;如果取消或失败,则恢复之前的 `kiro-cli` 会话。 | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。 | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。由于 CCA 不提供通用 `/models` 端点,因此使用维护中的六模型静态目录。 | | `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、HTTP/2 传输和按账号筛选的模型发现。 | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 实验性。GitHub 设备流 + `copilot_internal` 交换(VS Code OAuth 客户端)。需要有效的 Copilot 订阅;不是官方第三方 API。 | diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index 486b95b72..89bcd9404 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -98,6 +98,14 @@ export default function ProviderDetails({ const free = useMemo(() => isFreeProvider(item), [item]); const local = useMemo(() => isLocalProvider(item), [item]); const authSurface = useMemo(() => providerAuthSurface(item), [item]); + const connectionIdentity = JSON.stringify([ + codexController?.activeId ?? "", + accounts?.find(account => account.active)?.id ?? "", + keys?.find(entry => entry.active)?.id ?? "", + oauth?.loggedIn === undefined ? "" : String(oauth.loggedIn), + oauth?.needsReauth === undefined ? "" : String(oauth.needsReauth), + oauthEmail ?? "", + ]); const tabs = useMemo<{ id: Tab; label: string }[]>(() => [ { id: "overview", label: t("pws.tab.overview") }, { id: "models", label: t("pws.tab.models") }, @@ -230,6 +238,8 @@ export default function ProviderDetails({ /> ) : undefined} item={item} + apiBase={apiBase} + connectionIdentity={connectionIdentity} usageTotals={usageTotals} quotaReport={quotaReport} oauthEmail={oauthEmail} diff --git a/gui/src/components/provider-workspace/ProviderOverview.tsx b/gui/src/components/provider-workspace/ProviderOverview.tsx index ad35ac25d..5e6d998c9 100644 --- a/gui/src/components/provider-workspace/ProviderOverview.tsx +++ b/gui/src/components/provider-workspace/ProviderOverview.tsx @@ -3,6 +3,7 @@ * (STATS + Notes). Phase 030 of workspace design parity. */ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { readJsonOrThrow } from "../../fetch-json"; import { useT, useI18n } from "../../i18n/shared"; import { IconAlert, IconCheck } from "../../icons"; import { binProviderStatus, type WorkspaceItem } from "../../provider-workspace/catalog"; @@ -12,8 +13,24 @@ import type { ProviderUsageTotals } from "./types"; import { authModeLabel } from "./ProviderRail"; import type { ProviderUpdatePatch } from "./types"; +type ConnectionTestResult = { + applicable?: boolean; + ok?: boolean; + latencyMs?: number; + reason?: string; + message?: string; + error?: string; +}; + +type ConnectionTestState = { + key: string; + testing: boolean; + result: ConnectionTestResult | null; +}; + export default function ProviderOverview({ item, usageTotals, quotaReport, oauthEmail, + apiBase, connectionIdentity, onEditSettings, onViewUsage, onUpdateProvider, onReauthenticate, onCancelLogin, reauthBusy = false, accountPanel, @@ -22,6 +39,9 @@ export default function ProviderOverview({ usageTotals?: ProviderUsageTotals; quotaReport?: ProviderQuotaReportView; oauthEmail?: string; + apiBase?: string; + /** Opaque active credential identity used only to invalidate stale probe results. */ + connectionIdentity?: string; onEditSettings?: () => void; onViewUsage?: () => void; onUpdateProvider?: (name: string, patch: ProviderUpdatePatch) => Promise<{ ok: boolean; error?: string }>; @@ -48,6 +68,81 @@ export default function ProviderOverview({ const requests = usageTotals?.requests; const tokens = usageTotals?.totalTokens; const quota = accountQuotaFromReport(quotaReport); + const connectionProbeKey = JSON.stringify([ + apiBase ?? null, + item.name, + item.adapter, + item.baseUrl, + item.authMode ?? null, + item.apiKeyTransport ?? null, + item.liveModels ?? null, + item.disabled === true, + item.hasApiKey === true, + item.hasHeaders === true, + item.allowPrivateNetwork === true, + item.keyOptional === true, + item.activeNeedsReauth === true, + connectionIdentity ?? null, + ]); + const [connectionTest, setConnectionTest] = useState(null); + const connectionAbortRef = useRef<{ key: string; controller: AbortController } | null>(null); + const testingConnection = connectionTest?.key === connectionProbeKey && connectionTest.testing; + const connectionResult = connectionTest?.key === connectionProbeKey ? connectionTest.result : null; + + useEffect(() => { + return () => { + if (connectionAbortRef.current?.key === connectionProbeKey) { + connectionAbortRef.current.controller.abort(); + connectionAbortRef.current = null; + } + }; + }, [connectionProbeKey]); + + const testConnection = useCallback(async () => { + if (!apiBase) return; + connectionAbortRef.current?.controller.abort(); + const controller = new AbortController(); + connectionAbortRef.current = { key: connectionProbeKey, controller }; + setConnectionTest({ key: connectionProbeKey, testing: true, result: null }); + try { + const response = await fetch(`${apiBase}/api/providers/test?name=${encodeURIComponent(item.name)}`, { + method: "POST", + signal: controller.signal, + }); + const result = await readJsonOrThrow(response, t("pws.connectionFailed")); + if (!result) throw new Error(t("pws.connectionFailed")); + if (!controller.signal.aborted) { + setConnectionTest({ key: connectionProbeKey, testing: false, result }); + } + } catch (error) { + if (!controller.signal.aborted) { + setConnectionTest({ + key: connectionProbeKey, + testing: false, + result: { + applicable: true, + ok: false, + error: error instanceof Error ? error.message : t("pws.connectionFailed"), + }, + }); + } + } finally { + if (connectionAbortRef.current?.controller === controller) { + connectionAbortRef.current = null; + } + } + }, [apiBase, connectionProbeKey, item.name, t]); + + const connectionState = connectionResult?.applicable === false + ? "not-applicable" + : connectionResult?.ok === true + ? "ok" + : "failed"; + const connectionText = connectionResult?.applicable === false + ? t("pws.connectionNotApplicable") + : connectionResult?.ok === true + ? (connectionResult.message || t("pws.connectionOk")) + : (connectionResult?.error || t("pws.connectionFailed")); return (
@@ -82,6 +177,27 @@ export default function ProviderOverview({
)} + {apiBase && ( +
+ + {connectionResult && ( + + {connectionText} + + )} +
+ )} {onEditSettings && (