From 0627a2c255bc239d5748119f1d0bc4457a1c03e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 07:18:03 +0900 Subject: [PATCH 001/125] chore(release): open dev at 2.100.0 before releasing 2.99.0 (#8313) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index ddc9ff16b55..e8372b53288 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2633,7 +2633,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.99.0" +version = "2.100.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 201998c2a13..cc960d1bdc9 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.99.0" +version = "2.100.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 892d5c37df0..1e36a108fba 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.99.0", + "version": "2.100.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index 52c50ccd7f4..c6d5e47d21a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.99.0", + "version": "2.100.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 5279c91bf03b7a63f44556e79e1cba902ada9033 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 8 Nov 2026 14:58:38 +0900 Subject: [PATCH 002/125] fix(gui): configure Google Vertex ADC (#8311) * fix(vertex): support dashboard ADC configuration * fix(vertex): fence ambient credentials at dispatch * fix(vertex): address credential and setup review findings * fix(vertex): address final credential review * fix(vertex): address routing and discovery review * fix(vertex): preserve registry and catalog authority * fix(gui): satisfy Vertex form accessibility checks --- .../src/content/docs/ko/reference/adapters.md | 11 + .../src/content/docs/reference/adapters.md | 11 + .../content/docs/zh-cn/reference/adapters.md | 9 + gui/src/components/AddProviderModal.tsx | 1 + gui/src/components/add-provider-form-pane.tsx | 10 + .../provider-workspace/ProviderOverview.tsx | 11 +- .../ProviderOverviewDashboard.tsx | 5 +- .../provider-workspace/ProviderRail.tsx | 3 + .../provider-workspace/ProviderSettings.tsx | 24 +- .../ProviderWorkspaceShell.tsx | 11 +- .../components/provider-workspace/types.ts | 4 +- gui/src/design/pages/accounts.css | 4 + gui/src/hooks/useJsonConfigEditor.ts | 1 + gui/src/i18n/de.ts | 7 + gui/src/i18n/en.ts | 7 + gui/src/i18n/fr.ts | 7 + gui/src/i18n/ja.ts | 7 + gui/src/i18n/ko.ts | 7 + gui/src/i18n/pt.ts | 7 + gui/src/i18n/ru.ts | 7 + gui/src/i18n/tr.ts | 7 + gui/src/i18n/vi.ts | 7 + gui/src/i18n/zh-TW.ts | 7 + gui/src/i18n/zh.ts | 7 + gui/src/pages/providers-shared.ts | 4 + gui/src/provider-payload.ts | 13 + gui/src/provider-workspace/catalog.ts | 29 +- .../add-provider-subscription-cli.test.tsx | 45 ++ .../overview-refresh-all-quotas.test.tsx | 1 + .../provider-first-response-guidance.test.tsx | 16 + ...r-settings-live-models-provenance.test.tsx | 14 + .../provider-settings-request-pacing.test.tsx | 26 + gui/tests/use-json-config-editor.test.tsx | 6 +- src/adapters/google-http.ts | 3 + src/adapters/google-vertex-key-project.ts | 12 +- src/adapters/google.ts | 22 +- src/lib/gcp-adc.ts | 215 +++++-- src/providers/api-key-selection.ts | 10 +- src/providers/routed-config.ts | 33 +- src/providers/vertex-environment-authority.ts | 50 ++ src/router.ts | 1 + src/server/adapter-resolve.ts | 17 +- src/server/auth-cors.ts | 10 + src/server/management/provider-routes.ts | 8 + src/server/responses/adapter-continuation.ts | 1 + src/server/responses/fetch-helpers.ts | 5 + src/server/responses/request-transport.ts | 33 +- structure/gui-and-management-api.md | 2 +- structure/providers-and-adapters.md | 2 +- structure/providers/google.md | 9 + structure/runtime.md | 2 +- tests/adapters/google/gcp-adc.test.ts | 594 +++++++++++++++++- .../google/google-vertex-http.test.ts | 25 + .../google/google-vertex-key-project.test.ts | 5 + tests/adapters/google/vertex-catalog.test.ts | 32 + tests/gui/provider-workspace-data.test.ts | 28 + .../router-blocked-cross-provider.test.ts | 19 + 57 files changed, 1378 insertions(+), 96 deletions(-) create mode 100644 src/providers/vertex-environment-authority.ts diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 59466160810..cd6ed21d0b4 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -138,6 +138,17 @@ Ollama 공급자에 `adapter: "ollama-native"`로 설정할 수도 있습니다. 사용합니다. **인증:** `googleMode`에 따라 API 키, Vertex ADC, Google Antigravity OAuth 중 하나를 선택합니다. +대시보드의 Google Vertex AI 프리셋에서 프로젝트 ID와 리전을 선택적으로 입력할 수 있습니다. +API 키를 비우고 `GOOGLE_CLOUD_API_KEY`가 설정되지 않았다면 **프록시 호스트**의 ADC를 +사용합니다. ADC에는 프로젝트와 리전이 필요하며, 폼 또는 프록시 호스트의 +`GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT` 및 `GOOGLE_CLOUD_LOCATION`으로 설정합니다. +provider를 추가했다고 자격증명이나 권한의 유효성이 검증되는 것은 아닙니다. API 키에 +프로젝트를 지정하지 않으면 Vertex express 모드를 사용합니다. +대시보드 프리셋은 설정한 기본 모델로 시작하는 정적 목록을 사용하며, 연결 테스트는 Vertex +자격증명이나 권한을 검증하지 않습니다. +기존 Vertex 항목에서 `liveModels`를 생략하면 일반 검색 엔드포인트를 통한 기존 실시간 검색이 유지됩니다. +`liveModels: false`는 정적 목록을 사용하며 설정 화면에서 두 모드를 전환할 수 있습니다. + - 시스템 프롬프트 → `systemInstruction`; 메시지 → `contents[]`(assistant → `model`); 툴 → `functionDeclarations`. data URL 이미지 → `inline_data`. - Gemini가 tool-call id를 생략하면 합성합니다. Vertex와 Antigravity에서는 불투명한 diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 5eafc00c9a9..26ecb707983 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -431,6 +431,17 @@ MiMo model Command Code serves. `/v1beta/models/{model}:streamGenerateContent`; the other modes use their native Google endpoints. **Auth:** API key, Vertex ADC, or Google Antigravity OAuth, selected by `googleMode`. +For the Google Vertex AI dashboard preset, Project ID and Location are optional fields. With +an empty API key, the proxy uses ADC on the **proxy host**, unless `GOOGLE_CLOUD_API_KEY` is set; +that environment key takes precedence. ADC also needs a project and location, supplied in the +form or by `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` on the proxy +host. Adding the provider does not verify that credentials or permissions are usable. A key +without an explicit project uses Vertex express mode. +The dashboard preset uses a static catalog seeded by the configured default model; its connection +test does not verify Vertex credentials or permissions. +Existing Vertex rows without `liveModels` retain live discovery through the generic discovery endpoint. +Use `liveModels: false` for a static catalog; Settings can switch between the two modes. + - **Location denials are permission errors, not invalid requests.** Google rejects unsupported geographic or datacenter locations with HTTP 400 `FAILED_PRECONDITION: User location is not supported for the API use.` The proxy reports this as `… location not supported: …` and diff --git a/docs-site/src/content/docs/zh-cn/reference/adapters.md b/docs-site/src/content/docs/zh-cn/reference/adapters.md index 5af5494c331..b337ce9da29 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -119,6 +119,15 @@ interface ProviderAdapter { `/v1beta/models/{model}:streamGenerateContent`,其他模式使用各自的 Google 原生 endpoint。 **认证:** 根据 `googleMode` 选择 API key、Vertex ADC 或 Google Antigravity OAuth。 +在仪表盘的 Google Vertex AI 预设中,项目 ID 和位置为可选输入。API key 留空且未设置 +`GOOGLE_CLOUD_API_KEY` 时,代理使用 **代理主机**上的 ADC。ADC 还需要项目和位置,可在 +表单中填写,或由代理主机上的 `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT` 和 +`GOOGLE_CLOUD_LOCATION` 提供。添加 provider 不代表凭据或权限已验证。API key 未指定 +项目时使用 Vertex express 模式。 +仪表盘预设使用以配置的默认模型为起点的静态目录;连接测试不会验证 Vertex 凭据或权限。 +已有 Vertex 配置省略 `liveModels` 时会保留通过通用发现端点进行实时发现的原有行为。 +设置 `liveModels: false` 使用静态目录,也可在设置页面切换这两种模式。 + - 系统提示词 → `systemInstruction`;消息 → `contents[]`(assistant → `model`);工具 → `functionDeclarations`;data URL 图像 → `inline_data`。 - Gemini 省略 tool-call id 时会合成 id。Vertex 与 Antigravity 会保留并重放不透明 diff --git a/gui/src/components/AddProviderModal.tsx b/gui/src/components/AddProviderModal.tsx index 8e05222c45a..140e640c41c 100644 --- a/gui/src/components/AddProviderModal.tsx +++ b/gui/src/components/AddProviderModal.tsx @@ -174,6 +174,7 @@ export default function AddProviderModal({ apiKey: "", apiKeyTransport: undefined, defaultModel: p.defaultModel ?? "", + ...(p.id === "google-vertex" ? { googleMode: "vertex" as const, project: "", location: "" } : {}), allowPrivateNetwork: false, }, }); diff --git a/gui/src/components/add-provider-form-pane.tsx b/gui/src/components/add-provider-form-pane.tsx index e0e547a7bc1..4da3375fa26 100644 --- a/gui/src/components/add-provider-form-pane.tsx +++ b/gui/src/components/add-provider-form-pane.tsx @@ -183,6 +183,16 @@ export function AddProviderFormPane({ )} )} + {preset.id === "google-vertex" && form.adapter === "google" && <> +

{t("modal.vertexAdcHint")}

+

{t("modal.vertexStaticHint")}

+ + onFormChange({ ...form, project: e.target.value })} /> + + + onFormChange({ ...form, location: e.target.value })} placeholder="us-central1" /> + + } {!isReservedForward && onFormChange({ ...form, defaultModel: e.target.value })} placeholder={t("modal.defaultModelPlaceholder")} /> } diff --git a/gui/src/components/provider-workspace/ProviderOverview.tsx b/gui/src/components/provider-workspace/ProviderOverview.tsx index 4912b2ed86a..f2b0fc89c56 100644 --- a/gui/src/components/provider-workspace/ProviderOverview.tsx +++ b/gui/src/components/provider-workspace/ProviderOverview.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useRef, useState } 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"; +import { binProviderStatus, usesUnverifiedVertexCredentials, type WorkspaceItem } from "../../provider-workspace/catalog"; import { formatRequestCount, formatTokenCount } from "../../provider-workspace/usage"; import { modelVisible } from "../../model-visibility"; import { discoveryFailureLabel, type ModelRow } from "../../pages/models-shared"; @@ -79,17 +79,20 @@ export default function ProviderOverview({ const t = useT(); const { locale } = useI18n(); const status = binProviderStatus(item); + const vertexUnverified = usesUnverifiedVertexCredentials(item); const needsAttention = Boolean(item.activeNeedsReauth) || oauth?.needsReauth === true; const needsOAuthLogin = item.authMode === "oauth" && oauth?.loggedIn === false; const effectiveStatus = item.disabled === true ? "disabled" + : vertexUnverified && !needsAttention ? "unverified" : needsAttention || needsOAuthLogin || status === "needs-setup" ? "needs-setup" : "ready"; const statusText = effectiveStatus === "ready" ? t("pws.status.ready") + : effectiveStatus === "unverified" ? t("pws.vertexUnverified") : effectiveStatus === "needs-setup" - ? (needsAttention ? t("pws.status.needsAttention") : t("pws.status.needsSetup")) + ? (needsAttention ? t("pws.status.needsAttention") : vertexUnverified ? t("pws.vertexUnverified") : t("pws.status.needsSetup")) : t("prov.disabledBadge"); const requests = usageTotals?.requests; const tokens = usageTotals?.totalTokens; @@ -168,7 +171,7 @@ export default function ProviderOverview({ ? "needs-attention" : needsOAuthLogin ? "needs-login" - : effectiveStatus === "needs-setup" + : effectiveStatus === "needs-setup" || effectiveStatus === "unverified" ? "needs-setup" : "saved"; const providerStepText = providerStepState === "disabled" @@ -178,7 +181,7 @@ export default function ProviderOverview({ : providerStepState === "needs-login" ? t("pws.firstResponse.providerNeedsLogin") : providerStepState === "needs-setup" - ? t("pws.firstResponse.providerNeedsSetup") + ? t(vertexUnverified ? "pws.vertexSavedUnverified" : "pws.firstResponse.providerNeedsSetup") : t("pws.firstResponse.providerSaved"); const providerRows = (modelRows ?? []).filter(row => row.provider === item.name); diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx index 761cf54e83a..5e73899d4d9 100644 --- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx +++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx @@ -85,7 +85,7 @@ export default function ProviderOverviewDashboard({ }; const allItems = useMemo( - () => [...sections.ready, ...sections.needsSetup, ...sections.disabled], + () => [...sections.ready, ...sections.needsSetup, ...sections.unverified, ...sections.disabled], [sections], ); const knownNames = useMemo(() => new Set(allItems.map(p => p.name)), [allItems]); @@ -163,13 +163,14 @@ export default function ProviderOverviewDashboard({ {/* A calm stat strip (one surface, hairline dividers): the numbers stay neutral, a small dot carries the tone so colour never has to shout. */} -
+
0 ? t("pws.status.needsAttention") : t("pws.status.needsSetup")} tone="warn" /> + {sections.unverified.length > 0 && }
diff --git a/gui/src/components/provider-workspace/ProviderRail.tsx b/gui/src/components/provider-workspace/ProviderRail.tsx index b9a8d4d2c7b..91d5e00ea08 100644 --- a/gui/src/components/provider-workspace/ProviderRail.tsx +++ b/gui/src/components/provider-workspace/ProviderRail.tsx @@ -9,6 +9,7 @@ import { IconStar } from "../../icons"; import { binProviderStatus, isFreeProvider, + usesUnverifiedVertexCredentials, type WorkspaceItem, type WorkspaceProvider, } from "../../provider-workspace/catalog"; @@ -20,6 +21,7 @@ export function statusLabel(p: WorkspaceProvider, t: TFn): string { const s = binProviderStatus(p); if (s === "disabled") return t("prov.disabledBadge"); if (s === "ready") return t("pws.status.ready"); + if (usesUnverifiedVertexCredentials(p)) return t("pws.vertexUnverified"); if ("activeNeedsReauth" in p && (p as WorkspaceItem).activeNeedsReauth) { return t("pws.status.needsAttention"); } @@ -27,6 +29,7 @@ export function statusLabel(p: WorkspaceProvider, t: TFn): string { } export function authModeLabel(item: WorkspaceItem, t: TFn): string { + if (usesUnverifiedVertexCredentials(item)) return t("pws.vertexHostAuth"); // Configured with authMode "key" (the registry's authKind), but no key is ever used. if (isSubscriptionCliProvider(item)) return t("modal.badge.subscriptionCli"); switch (item.authMode) { diff --git a/gui/src/components/provider-workspace/ProviderSettings.tsx b/gui/src/components/provider-workspace/ProviderSettings.tsx index c00118d6844..f82e8c6dec0 100644 --- a/gui/src/components/provider-workspace/ProviderSettings.tsx +++ b/gui/src/components/provider-workspace/ProviderSettings.tsx @@ -21,7 +21,7 @@ import { isCatalogProviderId } from "../../provider-icons"; import { openAiAccountProviderState } from "../../provider-payload"; import { canConfigureResponsesReasoningReplay } from "../../responses-reasoning-replay"; import { ResponsesReasoningReplayControl } from "../responses-reasoning-replay-control"; -import { providerSupportsLiveModelDiscovery } from "../../provider-workspace/catalog"; +import { isVertexProvider, providerSupportsLiveModelDiscovery } from "../../provider-workspace/catalog"; import type { CatalogPreset } from "../provider-catalog/provider-presets"; import { authModeLabel } from "./ProviderRail"; import { ProviderProtocolPanel } from "./ProviderProtocolPanel"; @@ -85,6 +85,8 @@ export default function ProviderSettings({ const savedCursorHttpVersion = effectiveCursorHttpVersion(item.upstreamHttpVersion); const [adapter, setAdapter] = useState(item.adapter); const [baseUrl, setBaseUrl] = useState(item.baseUrl); + const [project, setProject] = useState(item.project ?? ""); + const [location, setLocation] = useState(item.location ?? ""); const [defaultModel, setDefaultModel] = useState(item.defaultModel ?? ""); const [authMode, setAuthMode] = useState(initialAuth); const [apiKeyTransport, setApiKeyTransport] = useState(item.apiKeyTransport ?? "x-api-key"); @@ -118,6 +120,8 @@ export default function ProviderSettings({ useEffect(() => { setAdapter(item.adapter); setBaseUrl(item.baseUrl); + setProject(item.project ?? ""); + setLocation(item.location ?? ""); setDefaultModel(item.defaultModel ?? ""); setAuthMode(String(item.authMode ?? (item.keyOptional ? "local" : "key"))); setApiKeyTransport(item.apiKeyTransport ?? "x-api-key"); @@ -135,7 +139,7 @@ export default function ProviderSettings({ setMsg(null); setModeMsg(null); queueMicrotask(() => setEndpointChoice(matchChoiceId(baseUrlChoices, item.baseUrl))); - }, [item.adapter, item.baseUrl, item.defaultModel, item.authMode, item.apiKeyTransport, item.keyOptional, item.note, item.allowPrivateNetwork, item.preserveResponsesReasoningContent, savedLiveModels, savedCursorHttpVersion, item.requestPacing, baseUrlChoices]); + }, [item.adapter, item.baseUrl, item.project, item.location, item.defaultModel, item.authMode, item.apiKeyTransport, item.keyOptional, item.note, item.allowPrivateNetwork, item.preserveResponsesReasoningContent, savedLiveModels, savedCursorHttpVersion, item.requestPacing, baseUrlChoices]); /* eslint-enable react-hooks/set-state-in-effect */ // Account mode syncs on its own: a mode PATCH refresh must not reset an in-progress @@ -209,8 +213,10 @@ export default function ProviderSettings({ const supportsReasoningReplay = !isCatalogProviderId(item.name) && canConfigureResponsesReasoningReplay({ adapter, baseUrl, authMode }); const reasoningReplayDirty = supportsReasoningReplay && preserveReasoningReplay !== (item.preserveResponsesReasoningContent === true); + const vertexSettings = isVertexProvider(item) && (item.name === "google-vertex" || adapter.trim() === "google"); const dirty = reasoningReplayDirty || adapter.trim() !== item.adapter || baseUrl.trim() !== item.baseUrl + || (vertexSettings && (project.trim() !== (item.project ?? "") || location.trim() !== (item.location ?? ""))) || defaultModel.trim() !== (item.defaultModel ?? "") || authMode !== String(item.authMode ?? (item.keyOptional ? "local" : "key")) || (adapter.trim() === "anthropic" && authMode === "key" && apiKeyTransport !== (item.apiKeyTransport ?? "x-api-key")) @@ -281,6 +287,8 @@ export default function ProviderSettings({ ...(pacingDirty ? { requestPacing: pacingDraft } : {}), }; if (!pacingOnly) { + if (vertexSettings && project.trim() !== (item.project ?? "")) patch.project = project.trim(); + if (vertexSettings && location.trim() !== (item.location ?? "")) patch.location = location.trim(); // Keep omitted legacy values omitted unless the user actually changes this toggle. // Otherwise an unrelated settings save manufactures `liveModels: true` provenance. if (liveModelDiscoverySupported && liveModels !== (item.liveModels !== false)) patch.liveModels = liveModels; @@ -342,7 +350,7 @@ export default function ProviderSettings({ }; const discard = () => { - setAdapter(item.adapter); setBaseUrl(item.baseUrl); + setAdapter(item.adapter); setBaseUrl(item.baseUrl); setProject(item.project ?? ""); setLocation(item.location ?? ""); setDefaultModel(item.defaultModel ?? ""); setAuthMode(initialAuth); setApiKeyTransport(item.apiKeyTransport ?? "x-api-key"); setNote(item.note ?? ""); setAllowPrivateNetwork(item.allowPrivateNetwork ?? false); setLiveModels(savedLiveModels); @@ -434,6 +442,16 @@ export default function ProviderSettings({ setBaseUrl(e.target.value)} readOnly={plainBaseUrlLocked} disabled={plainBaseUrlLocked} /> )} + {vertexSettings && <> + + + } {adapter.trim() === "cursor" && (