Skip to content
Open
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/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ ocx logout <provider>
| `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. |

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ja/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ ocx logout <provider>
| `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 ではありません。 |

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ko/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ ocx logout <provider>
| `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가 아닙니다. |

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ru/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ ocx logout <provider>
| `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. |

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/zh-cn/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ ocx logout <provider>
| `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。 |

Expand Down
10 changes: 10 additions & 0 deletions gui/src/components/provider-workspace/ProviderDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
Expand Down Expand Up @@ -230,6 +238,8 @@ export default function ProviderDetails({
/>
) : undefined}
item={item}
apiBase={apiBase}
connectionIdentity={connectionIdentity}
usageTotals={usageTotals}
quotaReport={quotaReport}
oauthEmail={oauthEmail}
Expand Down
116 changes: 116 additions & 0 deletions gui/src/components/provider-workspace/ProviderOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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 }>;
Expand All @@ -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<ConnectionTestState | null>(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<ConnectionTestResult>(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 (
<div className="pws-overview-layout">
<div className="pws-overview-main">
Expand Down Expand Up @@ -82,6 +177,27 @@ export default function ProviderOverview({
</div>
)}
</dl>
{apiBase && (
<div className="row" style={{ marginTop: 12, alignItems: "center" }}>
<button
type="button"
className="btn btn-ghost btn-sm"
disabled={testingConnection}
onClick={() => void testConnection()}
>
{testingConnection ? t("pws.testing") : t("pws.testConnection")}
</button>
{connectionResult && (
<span
role="status"
className={connectionState === "ok" ? "pws-status-ok" : connectionState === "failed" ? "pws-status-warn" : "muted"}
data-connection-test-state={connectionState}
>
{connectionText}
</span>
)}
</div>
)}
{onEditSettings && (
<button type="button" className="link-btn pws-edit-settings-link" onClick={onEditSettings}>
{t("pws.editSettings")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ export default function ProviderSettings({
setSaving(true);
setMsg(null);
try {
const patch: ProviderUpdatePatch = { adapter: adapter.trim(), baseUrl: nextBaseUrl, defaultModel: defaultModel.trim(), authMode, note: note.trim(), allowPrivateNetwork, liveModels };
const patch: ProviderUpdatePatch = { adapter: adapter.trim(), baseUrl: nextBaseUrl, defaultModel: defaultModel.trim(), authMode, note: note.trim(), allowPrivateNetwork };
// Keep omitted legacy values omitted unless the user actually changes this toggle.
// Otherwise an unrelated settings save manufactures `liveModels: true` provenance.
if (liveModels !== (item.liveModels !== false)) patch.liveModels = liveModels;
if (supportsApiKeyTransport) patch.apiKeyTransport = apiKeyTransport;
else if (item.apiKeyTransport !== undefined) patch.apiKeyTransport = "";
const res = await onUpdateProvider(item.name, patch);
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ export const de: Record<TKey, string> = {
"pws.testing": "Teste…",
"pws.connectionOk": "Verbindung OK",
"pws.connectionFailed": "Verbindung fehlgeschlagen",
"pws.connectionNotApplicable": "Nicht zutreffend — dieser Anbieter verwendet einen statischen Modellkatalog.",
"pws.editSettings": "Einstellungen bearbeiten",
"pws.viewUsage": "Detaillierte Nutzung anzeigen",
"pws.allSystemsOk": "Alle Systeme betriebsbereit",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,7 @@ export const en = {
"pws.testing": "Testing…",
"pws.connectionOk": "Connection OK",
"pws.connectionFailed": "Connection failed",
"pws.connectionNotApplicable": "Not applicable — this provider uses a static model catalog.",
"pws.editSettings": "Edit settings",
"pws.viewUsage": "View detailed usage",
"pws.allSystemsOk": "All systems operational",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ export const ja: Record<TKey, string> = {
"pws.testing": "テスト中…",
"pws.connectionOk": "接続 OK",
"pws.connectionFailed": "接続失敗",
"pws.connectionNotApplicable": "対象外 — このプロバイダーは静的モデルカタログを使用します。",
"pws.editSettings": "設定を編集",
"pws.viewUsage": "詳細な使用量を表示",
"pws.allSystemsOk": "すべてのシステムが稼働中",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,7 @@ export const ko: Record<TKey, string> = {
"pws.testing": "테스트 중…",
"pws.connectionOk": "연결 성공",
"pws.connectionFailed": "연결 실패",
"pws.connectionNotApplicable": "해당 없음 — 이 프로바이더는 정적 모델 카탈로그를 사용합니다.",
"pws.editSettings": "설정 편집",
"pws.viewUsage": "사용량 상세 보기",
"pws.allSystemsOk": "모든 시스템 정상",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,7 @@ export const ru: Record<TKey, string> = {
"pws.testing": "Проверка…",
"pws.connectionOk": "Подключение успешно",
"pws.connectionFailed": "Ошибка подключения",
"pws.connectionNotApplicable": "Не применимо — этот провайдер использует статический каталог моделей.",
"pws.editSettings": "Изменить настройки",
"pws.viewUsage": "Подробнее об использовании",
"pws.allSystemsOk": "Все системы работают штатно",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,7 @@ export const zh: Record<TKey, string> = {
"pws.testing": "测试中…",
"pws.connectionOk": "连接成功",
"pws.connectionFailed": "连接失败",
"pws.connectionNotApplicable": "不适用 — 此提供方使用静态模型目录。",
"pws.editSettings": "编辑设置",
"pws.viewUsage": "查看详细用量",
"pws.allSystemsOk": "所有系统正常运行",
Expand Down
Loading
Loading