From 87c1404bfbc9af9e62b28bdf0b3f3e293598ec7b Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Mon, 12 Oct 2026 17:08:17 +0800 Subject: [PATCH 1/8] feat(providers): support opencode go subscriptions --- .../src/adapters/http/routes/driver-route.ts | 13 ++ .../application/runtime-llm-proxy.service.ts | 17 +- .../runtime-vendor-proxy-env.builder.ts | 7 +- .../application/vendor-credential-probe.ts | 9 ++ apps/api/tests/driver-llm-proxy-route.test.ts | 145 ++++++++++++++++++ .../helpers/pi-proxy-protocol-fixture.ts | 6 +- .../api/tests/runtime-vendor-env-vars.test.ts | 1 + .../api/tests/vendor-credential-probe.test.ts | 27 ++++ apps/web/tests/runtime-default.test.ts | 13 ++ docs/prd/runtime-catalog.md | 4 + e2e/cases/ui/opencode-go.spec.ts | 29 ++++ pkgs/runtime-catalog/src/catalog.ts | 67 ++++++++ .../runtime-catalog/tests/opencode-go.test.ts | 41 +++++ 13 files changed, 371 insertions(+), 8 deletions(-) create mode 100644 e2e/cases/ui/opencode-go.spec.ts create mode 100644 pkgs/runtime-catalog/tests/opencode-go.test.ts diff --git a/apps/api/src/adapters/http/routes/driver-route.ts b/apps/api/src/adapters/http/routes/driver-route.ts index 585512a9..8c42873a 100644 --- a/apps/api/src/adapters/http/routes/driver-route.ts +++ b/apps/api/src/adapters/http/routes/driver-route.ts @@ -330,6 +330,18 @@ function buildLlmProxyUpstreamHeaders(incoming: Headers, target: RuntimeLlmProxy } } + if (target.vendor.vendorId === "opencode-go") { + // Bind Go routing/cache identity to the Host conversation, not a replaceable + // Driver or a sandbox-supplied header. The raw provider key stays in Host. + if (target.sessionId === undefined) + throw new RuntimeLlmProxyError("Go session is unavailable.", 403); + headers.set("User-Agent", "mosoo/1.0"); + headers.set("x-opencode-session", target.sessionId); + if (target.modelProtocol === "anthropic-messages" && !headers.has("anthropic-version")) { + headers.set("anthropic-version", "2023-06-01"); + } + } + const authHeader = target.vendor.vendorId === "openai-compatible" && ["anthropic-messages", "google-gemini"].includes(target.modelProtocol) @@ -576,6 +588,7 @@ export function registerDriverRoute(app: Hono) { try { target = await resolveRuntimeLlmProxyTarget(c.env, { credentialId: grant.resourceId, + driverInstanceId: grant.driverInstanceId, modelProtocol: grant.modelProtocol, projectId: grant.projectId, }); diff --git a/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts b/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts index 3220384d..51a8a21d 100644 --- a/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts +++ b/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts @@ -1,5 +1,5 @@ import type { PresetModelProtocol } from "@mosoo/contracts/models"; -import type { DriverInstanceId, ProjectId, VendorCredentialId } from "@mosoo/id"; +import type { DriverInstanceId, ProjectId, SessionId, VendorCredentialId } from "@mosoo/id"; import { getVendor } from "@mosoo/runtime-catalog"; import type { RuntimeCatalogVendor } from "@mosoo/runtime-catalog"; @@ -7,7 +7,10 @@ import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; import { isTruthy } from "../../../shared/truthiness"; import { readSecret } from "../../vault/application/vault-secret-store"; import { getProjectCredentialRow } from "../../vendor-credentials/application/vendor-credential.repository"; -import { isDriverInstanceGenerationActive } from "../infrastructure/driver-instance/driver-instance-record.repository"; +import { + getDriverInstanceRecord, + isDriverInstanceGenerationActive, +} from "../infrastructure/driver-instance/driver-instance-record.repository"; /** * Upstream target for one proxied model call. `apiKey` is the raw vendor @@ -16,6 +19,7 @@ import { isDriverInstanceGenerationActive } from "../infrastructure/driver-insta */ export interface RuntimeLlmProxyTarget { apiKey: string; + sessionId?: SessionId; modelProtocol: PresetModelProtocol; upstreamBaseUrl: string; vendor: RuntimeCatalogVendor; @@ -52,6 +56,7 @@ export async function resolveRuntimeLlmProxyTarget( bindings: ApiBindings, input: { credentialId: VendorCredentialId; + driverInstanceId: DriverInstanceId; modelProtocol: PresetModelProtocol; projectId: ProjectId; }, @@ -90,7 +95,15 @@ export async function resolveRuntimeLlmProxyTarget( throw new RuntimeLlmProxyError("Vendor upstream endpoint is not configured.", 502); } + let sessionId: SessionId | undefined; + if (vendor.vendorId === "opencode-go") { + const driver = await getDriverInstanceRecord(bindings.DB, input.driverInstanceId); + if (driver === null) throw new RuntimeLlmProxyError("Driver session is unavailable.", 403); + sessionId = driver.sandboxSessionId; + } + return { + ...(sessionId === undefined ? {} : { sessionId }), apiKey: await readSecret(bindings.DB, bindings, credential.apiKeySecretId), modelProtocol: input.modelProtocol, upstreamBaseUrl, diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts index 449aac87..aa47aace 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts @@ -267,10 +267,9 @@ function buildOpenCodeProviderConfig(input: OpenCodeProviderConfigInput): OpenCo return { models, name: provider.name, - npm: - input.vendor.vendorId === "openai-compatible" - ? OPENCODE_SDK_BY_PROTOCOL[input.modelProtocol] - : provider.npmPackage, + npm: ["openai-compatible", "opencode-go"].includes(input.vendor.vendorId) + ? OPENCODE_SDK_BY_PROTOCOL[input.modelProtocol] + : provider.npmPackage, options, }; } diff --git a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts index 11434969..4931737b 100644 --- a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts +++ b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts @@ -25,6 +25,15 @@ export function toVendorProbeAuthHeaders( apiKey: string, modelProtocol?: PresetModelProtocol | null, ): Record { + if (vendor.vendorId === "opencode-go") { + return { + Authorization: `Bearer ${apiKey}`, + "User-Agent": "mosoo/1.0", + // A manual credential probe is its own short-lived conversation. + "x-opencode-session": crypto.randomUUID(), + ...(modelProtocol === "anthropic-messages" ? { "anthropic-version": "2023-06-01" } : {}), + }; + } if (vendor.vendorId === "openai-compatible") { if (modelProtocol === "anthropic-messages") { return { "anthropic-version": "2023-06-01", "x-api-key": apiKey }; diff --git a/apps/api/tests/driver-llm-proxy-route.test.ts b/apps/api/tests/driver-llm-proxy-route.test.ts index b993e3f5..07572113 100644 --- a/apps/api/tests/driver-llm-proxy-route.test.ts +++ b/apps/api/tests/driver-llm-proxy-route.test.ts @@ -1215,3 +1215,148 @@ describe("driver LLM proxy route", () => { expect(await response.json()).toEqual({ error: "LLM proxy upstream request failed." }); }); }); + +describe("OpenCode Go subscription routing", () => { + test.each([ + ["openai-chat-completions", "/chat/completions", "deepseek-v4-flash"], + ["anthropic-messages", "/messages", "minimax-m2.7"], + ["openai-responses", "/responses", "gpt-5.6-luna"], + ] as const)( + "Given Go %s, When the Driver is replaced, Then proxy auth and conversation identity stay stable", + async (modelProtocol, path, modelId) => { + const { bindings, database } = await setupFixture({ vendorId: "opencode-go" }); + const captured = captureUpstreamFetch(); + for (const driverInstanceId of [DRIVER_INSTANCE_ID, OTHER_DRIVER_INSTANCE_ID]) { + if (driverInstanceId === OTHER_DRIVER_INSTANCE_ID) { + await database + .app() + .update(driverInstancesTable) + .set({ status: "failed" }) + .where(eq(driverInstancesTable.id, DRIVER_INSTANCE_ID)) + .run(); + await insertDriverInstance(database, "ready", { driverInstanceId }); + } + const grant = await createLlmProxyGrant(bindings, { + driverInstanceId, + modelProtocol, + modelId, + }); + const response = await dispatch( + bindings, + llmProxyRequest(path, { + method: "POST", + headers: { + authorization: `Bearer ${grant}`, + "content-type": "application/json", + "user-agent": "generic-sdk/1.0", + "x-opencode-session": "untrusted-session", + }, + body: JSON.stringify({ model: modelId, stream: true, messages: [], input: "test" }), + }), + ); + expect(response.status).toBe(200); + const upstream = captured.at(-1)!; + expect(upstream.url).toBe(`https://opencode.ai/zen/go/v1${path}`); + expect(upstream.headers.get("authorization")).toBe(`Bearer ${UPSTREAM_API_KEY}`); + expect(upstream.headers.get("user-agent")).toBe("mosoo/1.0"); + expect(upstream.headers.get("x-opencode-session")).toBe(PUBLIC_API_TEST_IDS.ownerSession); + expect(upstream.headers.get("x-api-key")).toBeNull(); + expect(JSON.stringify([...upstream.headers])).not.toContain(grant); + } + await database + .app() + .update(driverInstancesTable) + .set({ status: "failed" }) + .where(eq(driverInstancesTable.id, OTHER_DRIVER_INSTANCE_ID)) + .run(); + const revoked = await createLlmProxyGrant(bindings, { + driverInstanceId: OTHER_DRIVER_INSTANCE_ID, + modelProtocol, + modelId, + }); + expect( + ( + await dispatch( + bindings, + llmProxyRequest(path, { + method: "POST", + headers: { authorization: `Bearer ${revoked}` }, + body: JSON.stringify({ model: modelId }), + }), + ) + ).status, + ).toBe(403); + expect(captured).toHaveLength(2); + }, + ); +}); + +test.each([ + ["openai-chat-completions", "/chat/completions", "deepseek-v4-flash"], + ["anthropic-messages", "/messages", "minimax-m2.7"], + ["openai-responses", "/responses", "gpt-5.6-luna"], +] as const)( + "Given Go %s, When real OpenCode and Pi call a tool, Then the Host uses the Go wire contract", + async (modelProtocol, path, modelId) => { + const { bindings } = await setupFixture({ vendorId: "opencode-go" }); + for (const runtimeId of ["acp-fallback", "pi"] as const) { + let calls = 0; + const captured = captureUpstreamFetch(() => + piProxyProtocolResponse(modelProtocol, ++calls === 1), + ); + const proxy = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch: (request) => dispatch(bindings, request), + }); + try { + const env = await buildVendorProxyEnvVars({ + bindings, + driverGeneration: 0, + driverInstanceId: DRIVER_INSTANCE_ID, + profile: { + model: modelId, + modelProtocol, + runtimeId, + vendorCredential: { + apiBase: null, + credentialId: CREDENTIAL_ID, + projectId: PROJECT_ID, + models: [modelId], + vendorId: "opencode-go", + }, + }, + requestUrl: `http://127.0.0.1:${proxy.port}/api/sessions`, + }); + const events = + runtimeId === "pi" + ? await runPiProxyTurn(env, modelId) + : await runOpenCodeProxyTurn(env, `opencode-go/${modelId}`); + expect(calls).toBe(2); + expect(JSON.stringify(events)).toContain(PI_PROXY_TOOL_PROOF); + expect(JSON.stringify(events)).toContain(PI_PROXY_FINAL_TEXT); + expect(captured[1]?.body).toContain(PI_PROXY_TOOL_PROOF); + for (const request of captured) { + const upstreamUrl = new URL(request.url); + expect(upstreamUrl.origin + upstreamUrl.pathname).toBe( + `https://opencode.ai/zen/go/v1${path}`, + ); + if (upstreamUrl.search) { + expect(modelProtocol).toBe("anthropic-messages"); + expect(upstreamUrl.search).toBe("?beta=true"); + } + expect(request.headers.get("authorization")).toBe(`Bearer ${UPSTREAM_API_KEY}`); + expect(request.headers.get("x-api-key")).toBeNull(); + expect(request.headers.get("user-agent")).toBe("mosoo/1.0"); + expect(request.headers.get("x-opencode-session")).toBe(PUBLIC_API_TEST_IDS.ownerSession); + const grant = env[runtimeId === "pi" ? "MOSOO_PI_PROXY_GRANT" : "OPENCODE_GO_API_KEY"]; + expect(JSON.stringify([...request.headers])).not.toContain(grant); + expect(request.body).not.toContain(grant); + } + } finally { + await proxy.stop(true); + } + } + }, + 60_000, +); diff --git a/apps/api/tests/helpers/pi-proxy-protocol-fixture.ts b/apps/api/tests/helpers/pi-proxy-protocol-fixture.ts index cf3a4d95..dbf9e9af 100644 --- a/apps/api/tests/helpers/pi-proxy-protocol-fixture.ts +++ b/apps/api/tests/helpers/pi-proxy-protocol-fixture.ts @@ -190,6 +190,7 @@ export function piProxyProtocolResponse(protocol: PresetModelProtocol, tool: boo /** Real pinned Pi CLI, with all model traffic directed at the test's Host proxy. */ export async function runPiProxyTurn( variables: Record, + model = "pi-test", ): Promise[]> { const home = await mkdtemp(join(tmpdir(), "mosoo-pi-proxy-")); const agentDir = join(home, "pi"); @@ -216,7 +217,7 @@ export async function runPiProxyTurn( "--provider", "mosoo", "--model", - "pi-test", + model, "--offline", "--no-extensions", "--no-skills", @@ -293,6 +294,7 @@ export async function runPiProxyTurn( /** Real pinned OpenCode CLI using Host-rendered provider configuration. */ export async function runOpenCodeProxyTurn( variables: Record, + model = "openai-compatible/pi-test", ): Promise[]> { const home = await mkdtemp(join(tmpdir(), "mosoo-opencode-proxy-")); const executable = fileURLToPath( @@ -307,7 +309,7 @@ export async function runOpenCodeProxyTurn( "--format", "json", "--model", - "openai-compatible/pi-test", + model, "--title", "Protocol integration fixture", "Run the provided bash tool and then finish.", diff --git a/apps/api/tests/runtime-vendor-env-vars.test.ts b/apps/api/tests/runtime-vendor-env-vars.test.ts index d45c91ef..f477f5b1 100644 --- a/apps/api/tests/runtime-vendor-env-vars.test.ts +++ b/apps/api/tests/runtime-vendor-env-vars.test.ts @@ -78,6 +78,7 @@ describe("runtime vendor proxy env vars", () => { sanitizeRuntimeVendorEnvVars({ ANTHROPIC_API_KEY: "raw-anthropic-key", EXISTING_ENV: "kept", + OPENCODE_GO_API_KEY: "raw-go-key", OPENAI_BASE_URL: "https://attacker.example.com/v1", OPENCODE_CONFIG_CONTENT: '{"provider":{"openai":{"options":{"apiKey":"raw"}}}}', MOSOO_PI_CONFIG_CONTENT: diff --git a/apps/api/tests/vendor-credential-probe.test.ts b/apps/api/tests/vendor-credential-probe.test.ts index 6f3ff374..0ffdd0bc 100644 --- a/apps/api/tests/vendor-credential-probe.test.ts +++ b/apps/api/tests/vendor-credential-probe.test.ts @@ -276,3 +276,30 @@ describe("vendor credential probe", () => { }); } }); + +test("Given a Go key, When testing a model, Then the probe identifies Mosoo and uses Go auth", async () => { + const originalFetch = globalThis.fetch; + const requests: Request[] = []; + globalThis.fetch = async (url, init) => { + requests.push(new Request(url, init)); + return Response.json({ type: "message", content: [{ type: "text", text: "pong" }] }); + }; + try { + const result = await probeVendorCredential({ + apiKey: "probe-key", + modelId: "minimax-m2.7", + vendorId: "opencode-go", + verifyModelProtocol: true, + }); + expect(result.ok).toBe(true); + expect(requests).toHaveLength(1); + expect(requests[0].url).toBe("https://opencode.ai/zen/go/v1/messages"); + expect(requests[0].headers.get("authorization")).toBe("Bearer probe-key"); + expect(requests[0].headers.get("user-agent")).toBe("mosoo/1.0"); + expect(requests[0].headers.get("x-opencode-session")).toMatch(/^[a-f0-9-]{36}$/); + expect(requests[0].headers.get("anthropic-version")).toBe("2023-06-01"); + expect(requests[0].headers.has("x-api-key")).toBe(false); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/apps/web/tests/runtime-default.test.ts b/apps/web/tests/runtime-default.test.ts index 69ceaa9f..b2bd090b 100644 --- a/apps/web/tests/runtime-default.test.ts +++ b/apps/web/tests/runtime-default.test.ts @@ -159,3 +159,16 @@ describe("default agent runtime", () => { }); }); }); + +test("Given only a Go credential, When launching an Agent, Then OpenCode uses a Go model", () => { + expect(resolveDefaultAgentRuntime([credential("opencode-go")])).toEqual({ + model: "deepseek-v4-flash", + provider: "opencode-go", + runtimeId: "acp-fallback", + }); + expect(resolveDefaultAgentRuntime([credential("opencode-go")], "pi")).toEqual({ + model: "deepseek-v4-flash", + provider: "opencode-go", + runtimeId: "pi", + }); +}); diff --git a/docs/prd/runtime-catalog.md b/docs/prd/runtime-catalog.md index 7dfb4254..1faeb644 100644 --- a/docs/prd/runtime-catalog.md +++ b/docs/prd/runtime-catalog.md @@ -9,6 +9,8 @@ Which runtimes and models a Project can run, and how providers and model protoco - Every provider/model endpoint speaks one model protocol: Chat Completions, Responses, Anthropic Messages or Google Gemini. Preset models use the protocol in the catalog. A custom provider declares one, and new custom credentials default to Chat Completions. - OpenCode and Pi accept all four protocols, OpenAI Runtime only Responses, and Claude Agent SDK only Anthropic Messages for Anthropic models. A runtime and protocol pair that does not match is rejected before any execution. - Custom providers are `openai-compatible` in the API. When a Project's only usable keys are custom, a new Agent in the console defaults to OpenCode; Pi or OpenAI Runtime runs custom models only when chosen explicitly. Pi's default is an incomplete placeholder until a preset model or a model declared on a custom credential is selected. +- OpenCode Go is a separate provider from OpenCode Zen. Add a Go API key in Providers and select a Go model with OpenCode or Pi. The catalog contains a curated model list with each model's protocol; [OpenCode's Go documentation](https://opencode.ai/docs/go/) defines the upstream endpoints and subscription limits. Mosoo does not sell or manage the subscription. +- Go requests identify Mosoo and use the Host Session ID for routing and caching, including after Driver replacement. Manual credential tests use a separate temporary conversation ID. Provider keys stay behind the Host proxy. - Publishing does not lock an Agent's runtime or model. ## Limits @@ -18,3 +20,5 @@ Which runtimes and models a Project can run, and how providers and model protoco - A matching protocol does not prove that an endpoint supports the streaming and tool calls a runtime needs. - Changing a custom credential's protocol breaks the Sessions that use it: their model calls are refused, and continuing them fails with a request to restore the protocol or start a new Session. A native conversation never switches protocols. - A custom credential saved without a protocol keeps its old per-runtime route (Responses on OpenAI Runtime, Chat Completions elsewhere) until one is declared. A declared protocol cannot be cleared. + +Go model usage is subject to the external subscription and its configured overage policy. Mosoo's Cost view estimates model usage; it does not represent remaining Go quota or subscription charges. diff --git a/e2e/cases/ui/opencode-go.spec.ts b/e2e/cases/ui/opencode-go.spec.ts new file mode 100644 index 00000000..fe69c927 --- /dev/null +++ b/e2e/cases/ui/opencode-go.spec.ts @@ -0,0 +1,29 @@ +import { mkdirSync } from "node:fs"; + +import { expect, test } from "@playwright/test"; + +import { installConsoleFixtures } from "../../lib/console-fixtures"; + +for (const width of [1440, 390]) { + test(`Go credentials stay separate from Zen at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await installConsoleFixtures(page); + await page.goto("/providers"); + + const go = page.locator("section").filter({ + has: page.getByRole("heading", { name: "OpenCode Go", exact: true }), + }); + await expect(go).toBeVisible(); + await expect(page.getByRole("heading", { name: "OpenCode Zen", exact: true })).toBeVisible(); + const output = ".tmp/e2e/opencode-go"; + mkdirSync(output, { recursive: true }); + await go.screenshot({ path: `${output}/provider-${width}.png` }); + await go.getByRole("button", { name: "Add key", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog.getByRole("heading", { name: "Add OpenCode Go key" })).toBeVisible(); + await expect(dialog.getByLabel("API key", { exact: true })).toBeVisible(); + await expect(dialog.getByLabel("Base URL", { exact: true })).toHaveCount(0); + await expect(dialog.getByRole("button", { name: "Save", exact: true })).toBeVisible(); + await dialog.screenshot({ path: `${output}/dialog-${width}.png` }); + }); +} diff --git a/pkgs/runtime-catalog/src/catalog.ts b/pkgs/runtime-catalog/src/catalog.ts index 9a9578af..9d394c46 100644 --- a/pkgs/runtime-catalog/src/catalog.ts +++ b/pkgs/runtime-catalog/src/catalog.ts @@ -184,6 +184,16 @@ const VENDOR_OPENCODE: RuntimeCatalogVendor = { vendorId: "opencode", }; +const VENDOR_OPENCODE_GO: RuntimeCatalogVendor = { + apiKeyEnvVar: "OPENCODE_GO_API_KEY", + authHeader: BEARER_AUTH, + defaultApiBase: "https://opencode.ai/zen/go/v1", + iconKey: "opencode", + label: "OpenCode Go", + openCodeProvider: { name: "OpenCode Go", npmPackage: "@ai-sdk/openai-compatible" }, + vendorId: "opencode-go", +}; + export const VENDORS: readonly RuntimeCatalogVendor[] = [ VENDOR_ANTHROPIC, VENDOR_OPENAI, @@ -195,6 +205,7 @@ export const VENDORS: readonly RuntimeCatalogVendor[] = [ VENDOR_ZHIPU, VENDOR_MINIMAX, VENDOR_OPENCODE, + VENDOR_OPENCODE_GO, ]; export const MODEL_DEFAULTS: Readonly> = { @@ -204,6 +215,7 @@ export const MODEL_DEFAULTS: Readonly> = { kimi: "kimi-k2.6", minimax: "MiniMax-M3", opencode: "deepseek-v4-pro", + "opencode-go": "deepseek-v4-flash", openai: "gpt-5.5", qwen: "qwen3.7-plus", zhipu: "glm-4.7", @@ -402,6 +414,60 @@ export const PRESET_MODELS: readonly RuntimeCatalogPresetModel[] = [ protocol: "google-gemini", vendor: VENDOR_OPENCODE, }, + { + displayName: "DeepSeek V4 Flash", + modelId: "deepseek-v4-flash", + protocol: "openai-chat-completions", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "DeepSeek V4 Pro", + modelId: "deepseek-v4-pro", + protocol: "openai-chat-completions", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "GLM 5.2", + modelId: "glm-5.2", + protocol: "openai-chat-completions", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "Kimi K2.6", + modelId: "kimi-k2.6", + protocol: "openai-chat-completions", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "Kimi K2.7 Code", + modelId: "kimi-k2.7-code", + protocol: "openai-chat-completions", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "MiniMax M3", + modelId: "minimax-m3", + protocol: "anthropic-messages", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "MiniMax M2.7", + modelId: "minimax-m2.7", + protocol: "anthropic-messages", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "Qwen3.7 Plus", + modelId: "qwen3.7-plus", + protocol: "anthropic-messages", + vendor: VENDOR_OPENCODE_GO, + }, + { + displayName: "GPT 5.6 Luna", + modelId: "gpt-5.6-luna", + protocol: "openai-responses", + vendor: VENDOR_OPENCODE_GO, + }, ]; const ALL_MODEL_PROTOCOLS: readonly PresetModelProtocol[] = [ @@ -421,6 +487,7 @@ const MULTI_PROVIDER_VENDORS: readonly RuntimeCatalogVendor[] = [ VENDOR_ZHIPU, VENDOR_MINIMAX, VENDOR_OPENCODE, + VENDOR_OPENCODE_GO, ]; export const RUNTIMES: readonly RuntimeCatalogRuntime[] = [ diff --git a/pkgs/runtime-catalog/tests/opencode-go.test.ts b/pkgs/runtime-catalog/tests/opencode-go.test.ts new file mode 100644 index 00000000..929e5e2c --- /dev/null +++ b/pkgs/runtime-catalog/tests/opencode-go.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test"; + +import { + PUBLIC_VENDORS, + getDefaultModelIdForVendor, + getVendor, + resolveRuntimeModelProtocol, +} from "@mosoo/runtime-catalog"; + +test("Given a Go subscription, When choosing a provider, Then Go stays distinct from Zen", () => { + expect(PUBLIC_VENDORS.find((vendor) => vendor.vendorId === "opencode-go")).toMatchObject({ + label: "OpenCode Go", + iconKey: "opencode", + defaultApiBase: "https://opencode.ai/zen/go/v1", + apiKeyEnvVar: "OPENCODE_GO_API_KEY", + }); + expect(getVendor("opencode")?.defaultApiBase).toBe("https://opencode.ai/zen/v1"); + expect(getDefaultModelIdForVendor("opencode-go")).toBe("deepseek-v4-flash"); +}); + +test.each(["acp-fallback", "pi"])( + "Given Go models, When %s admits them, Then their official protocols are preserved", + (runtimeId) => { + for (const [modelId, modelProtocol] of [ + ["deepseek-v4-flash", "openai-chat-completions"], + ["minimax-m2.7", "anthropic-messages"], + ["gpt-5.6-luna", "openai-responses"], + ]) { + expect( + resolveRuntimeModelProtocol({ runtimeId, vendorId: "opencode-go", modelId: modelId }), + ).toEqual({ ok: true, modelProtocol }); + } + expect( + resolveRuntimeModelProtocol({ + runtimeId, + vendorId: "opencode-go", + modelId: "gemini-3.5-flash", + }), + ).toMatchObject({ ok: false, code: "model-unknown" }); + }, +); From 6c3be94a883e7cd4a8c1fc1bc97415fa2b9b5056 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Mon, 12 Oct 2026 17:23:09 +0800 Subject: [PATCH 2/8] fix(providers): identify the deployed worker version --- .../src/adapters/http/routes/driver-route.ts | 2 +- .../application/runtime-llm-proxy.service.ts | 3 ++ .../application/vendor-credential-probe.ts | 4 ++- .../application/vendor-credential-test.ts | 15 +++++++-- .../platform/cloudflare/worker-user-agent.ts | 4 +++ apps/api/tests/driver-llm-proxy-route.test.ts | 32 +++++++++++++++++-- .../api/tests/vendor-credential-probe.test.ts | 3 +- apps/api/tests/worker-user-agent.test.ts | 22 +++++++++++++ apps/api/wrangler.toml | 9 ++++++ docs/prd/runtime-catalog.md | 2 +- 10 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/platform/cloudflare/worker-user-agent.ts create mode 100644 apps/api/tests/worker-user-agent.test.ts diff --git a/apps/api/src/adapters/http/routes/driver-route.ts b/apps/api/src/adapters/http/routes/driver-route.ts index 8c42873a..7a6dde79 100644 --- a/apps/api/src/adapters/http/routes/driver-route.ts +++ b/apps/api/src/adapters/http/routes/driver-route.ts @@ -335,7 +335,7 @@ function buildLlmProxyUpstreamHeaders(incoming: Headers, target: RuntimeLlmProxy // Driver or a sandbox-supplied header. The raw provider key stays in Host. if (target.sessionId === undefined) throw new RuntimeLlmProxyError("Go session is unavailable.", 403); - headers.set("User-Agent", "mosoo/1.0"); + headers.set("User-Agent", target.userAgent); headers.set("x-opencode-session", target.sessionId); if (target.modelProtocol === "anthropic-messages" && !headers.has("anthropic-version")) { headers.set("anthropic-version", "2023-06-01"); diff --git a/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts b/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts index 51a8a21d..e1bd468b 100644 --- a/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts +++ b/apps/api/src/modules/runtime/application/runtime-llm-proxy.service.ts @@ -4,6 +4,7 @@ import { getVendor } from "@mosoo/runtime-catalog"; import type { RuntimeCatalogVendor } from "@mosoo/runtime-catalog"; import type { ApiBindings } from "../../../platform/cloudflare/worker-types"; +import { getWorkerUserAgent } from "../../../platform/cloudflare/worker-user-agent"; import { isTruthy } from "../../../shared/truthiness"; import { readSecret } from "../../vault/application/vault-secret-store"; import { getProjectCredentialRow } from "../../vendor-credentials/application/vendor-credential.repository"; @@ -19,6 +20,7 @@ import { */ export interface RuntimeLlmProxyTarget { apiKey: string; + userAgent: string; sessionId?: SessionId; modelProtocol: PresetModelProtocol; upstreamBaseUrl: string; @@ -104,6 +106,7 @@ export async function resolveRuntimeLlmProxyTarget( return { ...(sessionId === undefined ? {} : { sessionId }), + userAgent: getWorkerUserAgent(bindings.CF_VERSION_METADATA), apiKey: await readSecret(bindings.DB, bindings, credential.apiKeySecretId), modelProtocol: input.modelProtocol, upstreamBaseUrl, diff --git a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts index 4931737b..7d57a7a9 100644 --- a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts +++ b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts @@ -1,6 +1,7 @@ import type { PresetModelProtocol } from "@mosoo/contracts/models"; import type { RuntimeCatalogVendor } from "@mosoo/runtime-catalog"; +import { getWorkerUserAgent } from "../../../platform/cloudflare/worker-user-agent"; import type { ProviderFetchProxyConfig } from "./provider-fetch-proxy"; import { fetchViaProviderProxy } from "./provider-fetch-proxy"; @@ -24,11 +25,12 @@ export function toVendorProbeAuthHeaders( vendor: RuntimeCatalogVendor, apiKey: string, modelProtocol?: PresetModelProtocol | null, + versionMetadata?: Pick, ): Record { if (vendor.vendorId === "opencode-go") { return { Authorization: `Bearer ${apiKey}`, - "User-Agent": "mosoo/1.0", + "User-Agent": getWorkerUserAgent(versionMetadata), // A manual credential probe is its own short-lived conversation. "x-opencode-session": crypto.randomUUID(), ...(modelProtocol === "anthropic-messages" ? { "anthropic-version": "2023-06-01" } : {}), diff --git a/apps/api/src/modules/vendor-credentials/application/vendor-credential-test.ts b/apps/api/src/modules/vendor-credentials/application/vendor-credential-test.ts index fea6dd81..2150ef35 100644 --- a/apps/api/src/modules/vendor-credentials/application/vendor-credential-test.ts +++ b/apps/api/src/modules/vendor-credentials/application/vendor-credential-test.ts @@ -37,6 +37,7 @@ export interface VendorCredentialProbeInput { timeoutMs?: number; vendorId: string; verifyModelProtocol?: boolean; + versionMetadata?: Pick; } function modelProbeRequest( @@ -118,6 +119,7 @@ async function probeModelProtocol(input: { timeoutMs: number; vendor: RuntimeCatalogVendor; verifyResponse: boolean; + versionMetadata: Pick | undefined; }): Promise<{ errorCode?: string; ok: boolean }> { const request = modelProbeRequest(input.modelId, input.modelProtocol); const url = new URL(input.baseUrl); @@ -136,7 +138,12 @@ async function probeModelProtocol(input: { headers: { Accept: "application/json", "Content-Type": "application/json", - ...toVendorProbeAuthHeaders(input.vendor, input.apiKey, input.modelProtocol), + ...toVendorProbeAuthHeaders( + input.vendor, + input.apiKey, + input.modelProtocol, + input.versionMetadata, + ), }, method: "POST", }, @@ -208,6 +215,7 @@ export async function testVendorCredential( const result = await probeVendorCredential({ ...input, + versionMetadata: bindings.CF_VERSION_METADATA, fetchProxy: resolveProviderFetchProxy(bindings), modelProtocol: normalizeCredentialModelProtocol(input.vendorId, input.modelProtocol), verifyModelProtocol: Boolean(input.modelId?.trim()), @@ -303,6 +311,7 @@ export async function probeVendorCredential( modelProtocol === null ? { errorCode: "unknown_model_protocol", ok: false } : await probeModelProtocol({ + versionMetadata: input.versionMetadata, apiKey, baseUrl, fetchProxy, @@ -325,7 +334,7 @@ export async function probeVendorCredential( { headers: { Accept: "application/json", - ...toVendorProbeAuthHeaders(vendor, apiKey, modelProtocol), + ...toVendorProbeAuthHeaders(vendor, apiKey, modelProtocol, input.versionMetadata), }, method: "GET", }, @@ -343,6 +352,7 @@ export async function probeVendorCredential( if (!ok) { if (modelProtocol !== null) { const modelProbe = await probeModelProtocol({ + versionMetadata: input.versionMetadata, apiKey, baseUrl, fetchProxy, @@ -365,6 +375,7 @@ export async function probeVendorCredential( errorCode = "missing_model_id"; } else if (modelProtocol !== null) { const modelProbe = await probeModelProtocol({ + versionMetadata: input.versionMetadata, apiKey, baseUrl, fetchProxy, diff --git a/apps/api/src/platform/cloudflare/worker-user-agent.ts b/apps/api/src/platform/cloudflare/worker-user-agent.ts new file mode 100644 index 00000000..e8e85f09 --- /dev/null +++ b/apps/api/src/platform/cloudflare/worker-user-agent.ts @@ -0,0 +1,4 @@ +/** Use the deployed Worker revision; local callers have no deployed version. */ +export function getWorkerUserAgent(metadata?: Pick): string { + return `mosoo/${metadata?.id || "dev"}`; +} diff --git a/apps/api/tests/driver-llm-proxy-route.test.ts b/apps/api/tests/driver-llm-proxy-route.test.ts index 07572113..252c990c 100644 --- a/apps/api/tests/driver-llm-proxy-route.test.ts +++ b/apps/api/tests/driver-llm-proxy-route.test.ts @@ -3,16 +3,18 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; import type { PresetModelProtocol } from "@mosoo/contracts/models"; import { driverInstancesTable, vendorCredentialsTable } from "@mosoo/db"; import { parsePlatformId } from "@mosoo/id"; -import type { DriverInstanceId, ProjectId, VendorCredentialId } from "@mosoo/id"; +import type { AccountId, DriverInstanceId, ProjectId, VendorCredentialId } from "@mosoo/id"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; import { registerDriverRoute } from "../src/adapters/http/routes/driver-route"; +import { getAccountViewer } from "../src/modules/auth/application/viewer-auth.service"; import { getRuntimeDriverLlmProxyPath } from "../src/modules/runtime/domain/runtime-driver-routes"; import { createRuntimeActionToken } from "../src/modules/runtime/infrastructure/runtime-boot-token"; import type { RuntimeActionTokenPayload } from "../src/modules/runtime/infrastructure/runtime-boot-token"; import { buildVendorProxyEnvVars } from "../src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder"; import { storeSecret } from "../src/modules/vault/application/vault-secret-store"; +import { testVendorCredential } from "../src/modules/vendor-credentials/application/vendor-credential-test"; import { runWithRequestLogContext } from "../src/platform/cloudflare/logger"; import type { ApiBindings, ApiGatewayEnvironment } from "../src/platform/cloudflare/worker-types"; import { @@ -1225,9 +1227,11 @@ describe("OpenCode Go subscription routing", () => { "Given Go %s, When the Driver is replaced, Then proxy auth and conversation identity stay stable", async (modelProtocol, path, modelId) => { const { bindings, database } = await setupFixture({ vendorId: "opencode-go" }); + bindings.CF_VERSION_METADATA = { id: "go-release-a", tag: "", timestamp: "" }; const captured = captureUpstreamFetch(); for (const driverInstanceId of [DRIVER_INSTANCE_ID, OTHER_DRIVER_INSTANCE_ID]) { if (driverInstanceId === OTHER_DRIVER_INSTANCE_ID) { + bindings.CF_VERSION_METADATA = { id: "go-release-b", tag: "", timestamp: "" }; await database .app() .update(driverInstancesTable) @@ -1258,7 +1262,7 @@ describe("OpenCode Go subscription routing", () => { const upstream = captured.at(-1)!; expect(upstream.url).toBe(`https://opencode.ai/zen/go/v1${path}`); expect(upstream.headers.get("authorization")).toBe(`Bearer ${UPSTREAM_API_KEY}`); - expect(upstream.headers.get("user-agent")).toBe("mosoo/1.0"); + expect(upstream.headers.get("user-agent")).toBe(`mosoo/${bindings.CF_VERSION_METADATA.id}`); expect(upstream.headers.get("x-opencode-session")).toBe(PUBLIC_API_TEST_IDS.ownerSession); expect(upstream.headers.get("x-api-key")).toBeNull(); expect(JSON.stringify([...upstream.headers])).not.toContain(grant); @@ -1299,6 +1303,7 @@ test.each([ "Given Go %s, When real OpenCode and Pi call a tool, Then the Host uses the Go wire contract", async (modelProtocol, path, modelId) => { const { bindings } = await setupFixture({ vendorId: "opencode-go" }); + bindings.CF_VERSION_METADATA = { id: "go-cli-release", tag: "", timestamp: "" }; for (const runtimeId of ["acp-fallback", "pi"] as const) { let calls = 0; const captured = captureUpstreamFetch(() => @@ -1347,7 +1352,9 @@ test.each([ } expect(request.headers.get("authorization")).toBe(`Bearer ${UPSTREAM_API_KEY}`); expect(request.headers.get("x-api-key")).toBeNull(); - expect(request.headers.get("user-agent")).toBe("mosoo/1.0"); + expect(request.headers.get("user-agent")).toBe( + `mosoo/${bindings.CF_VERSION_METADATA.id}`, + ); expect(request.headers.get("x-opencode-session")).toBe(PUBLIC_API_TEST_IDS.ownerSession); const grant = env[runtimeId === "pi" ? "MOSOO_PI_PROXY_GRANT" : "OPENCODE_GO_API_KEY"]; expect(JSON.stringify([...request.headers])).not.toContain(grant); @@ -1360,3 +1367,22 @@ test.each([ }, 60_000, ); + +test("Given Host version metadata, When testing a Go credential, Then the probe uses that deployment", async () => { + const { bindings } = await setupFixture({ vendorId: "opencode-go" }); + bindings.CF_VERSION_METADATA = { id: "go-host-probe", tag: "", timestamp: "" }; + const viewer = await getAccountViewer(bindings.DB, PUBLIC_API_TEST_IDS.ownerAccount as AccountId); + if (viewer === null) throw new Error("Missing fixture viewer"); + const captured = captureUpstreamFetch(() => + Response.json({ choices: [{ message: { content: "pong" } }] }), + ); + const result = await testVendorCredential(bindings, viewer, { + projectId: PROJECT_ID, + apiKey: "probe-key", + modelId: "deepseek-v4-flash", + vendorId: "opencode-go", + }); + expect(result.ok).toBe(true); + expect(captured).toHaveLength(1); + expect(captured[0]?.headers.get("user-agent")).toBe("mosoo/go-host-probe"); +}); diff --git a/apps/api/tests/vendor-credential-probe.test.ts b/apps/api/tests/vendor-credential-probe.test.ts index 0ffdd0bc..ec050b9c 100644 --- a/apps/api/tests/vendor-credential-probe.test.ts +++ b/apps/api/tests/vendor-credential-probe.test.ts @@ -289,13 +289,14 @@ test("Given a Go key, When testing a model, Then the probe identifies Mosoo and apiKey: "probe-key", modelId: "minimax-m2.7", vendorId: "opencode-go", + versionMetadata: { id: "go-probe-release" }, verifyModelProtocol: true, }); expect(result.ok).toBe(true); expect(requests).toHaveLength(1); expect(requests[0].url).toBe("https://opencode.ai/zen/go/v1/messages"); expect(requests[0].headers.get("authorization")).toBe("Bearer probe-key"); - expect(requests[0].headers.get("user-agent")).toBe("mosoo/1.0"); + expect(requests[0].headers.get("user-agent")).toBe("mosoo/go-probe-release"); expect(requests[0].headers.get("x-opencode-session")).toMatch(/^[a-f0-9-]{36}$/); expect(requests[0].headers.get("anthropic-version")).toBe("2023-06-01"); expect(requests[0].headers.has("x-api-key")).toBe(false); diff --git a/apps/api/tests/worker-user-agent.test.ts b/apps/api/tests/worker-user-agent.test.ts new file mode 100644 index 00000000..10ebc79f --- /dev/null +++ b/apps/api/tests/worker-user-agent.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +import { getWorkerUserAgent } from "../src/platform/cloudflare/worker-user-agent"; + +test("Given no deployment metadata, When running locally, Then identify a development build", () => { + expect(getWorkerUserAgent()).toBe("mosoo/dev"); + expect(getWorkerUserAgent({ id: "" })).toBe("mosoo/dev"); +}); + +test("Given a new Worker deployment, When constructing client identity, Then use its version ID", () => { + expect(getWorkerUserAgent({ id: "release-a" })).toBe("mosoo/release-a"); + expect(getWorkerUserAgent({ id: "release-b" })).toBe("mosoo/release-b"); +}); + +test("Given any API environment, When deployed, Then Cloudflare injects version metadata", () => { + const config = Bun.TOML.parse(readFileSync(new URL("../wrangler.toml", import.meta.url), "utf8")); + const environments = config.env as Record>; + for (const environment of [config, environments.stage, environments.prod]) { + expect(environment?.version_metadata).toEqual({ binding: "CF_VERSION_METADATA" }); + } +}); diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml index b4592ed2..b40d95a2 100644 --- a/apps/api/wrangler.toml +++ b/apps/api/wrangler.toml @@ -4,6 +4,9 @@ compatibility_date = "2026-06-01" compatibility_flags = ["nodejs_compat"] keep_vars = true +[version_metadata] +binding = "CF_VERSION_METADATA" + [observability] enabled = true @@ -157,6 +160,9 @@ workers_dev = true # Stage mirrors production behavior and topology but uses isolated resources # and synthetic data behind its workers.dev origin. +[env.stage.version_metadata] +binding = "CF_VERSION_METADATA" + [env.stage.observability] enabled = true @@ -308,6 +314,9 @@ dead_letter_queue = "mosoo-stage-api-command-dlq" name = "mosoo-api-prod" workers_dev = false +[env.prod.version_metadata] +binding = "CF_VERSION_METADATA" + # Named envs do not inherit the top-level [observability] block, so prod had no # queryable Workers Logs (only traces). Enable logs so structured log lines # (e.g. "closed before ready", driver/ready/socket events) are retrievable. diff --git a/docs/prd/runtime-catalog.md b/docs/prd/runtime-catalog.md index 1faeb644..6941370d 100644 --- a/docs/prd/runtime-catalog.md +++ b/docs/prd/runtime-catalog.md @@ -10,7 +10,7 @@ Which runtimes and models a Project can run, and how providers and model protoco - OpenCode and Pi accept all four protocols, OpenAI Runtime only Responses, and Claude Agent SDK only Anthropic Messages for Anthropic models. A runtime and protocol pair that does not match is rejected before any execution. - Custom providers are `openai-compatible` in the API. When a Project's only usable keys are custom, a new Agent in the console defaults to OpenCode; Pi or OpenAI Runtime runs custom models only when chosen explicitly. Pi's default is an incomplete placeholder until a preset model or a model declared on a custom credential is selected. - OpenCode Go is a separate provider from OpenCode Zen. Add a Go API key in Providers and select a Go model with OpenCode or Pi. The catalog contains a curated model list with each model's protocol; [OpenCode's Go documentation](https://opencode.ai/docs/go/) defines the upstream endpoints and subscription limits. Mosoo does not sell or manage the subscription. -- Go requests identify Mosoo and use the Host Session ID for routing and caching, including after Driver replacement. Manual credential tests use a separate temporary conversation ID. Provider keys stay behind the Host proxy. +- Go requests identify Mosoo with the current Cloudflare Worker version ID (`mosoo/dev` for local callers without deployment metadata) and use the Host Session ID for routing and caching, including after Driver replacement. Manual credential tests use a separate temporary conversation ID. Provider keys stay behind the Host proxy. - Publishing does not lock an Agent's runtime or model. ## Limits From e495d2deb7f65074a8e9906b5113b76ef83cd05b Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Tue, 20 Oct 2026 21:45:43 +0800 Subject: [PATCH 3/8] fix(providers): redact go keys and normalize usage replay ids --- .../session-event-read.repository.ts | 10 ++++++---- .../application/vendor-credential-probe.ts | 2 +- apps/api/tests/session-process-events.test.ts | 4 +++- apps/api/tests/vendor-credential-probe.test.ts | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/api/src/modules/sessions/infrastructure/session-event-read.repository.ts b/apps/api/src/modules/sessions/infrastructure/session-event-read.repository.ts index 54b5aabb..1e29eeeb 100644 --- a/apps/api/src/modules/sessions/infrastructure/session-event-read.repository.ts +++ b/apps/api/src/modules/sessions/infrastructure/session-event-read.repository.ts @@ -160,10 +160,12 @@ export async function readSessionEventRowsAfterSeq( usage_key: sql`CASE WHEN ${sessionEventsTable.eventType} = 'usage.updated' AND ${sessionEventsTable.canonicalEventJson} IS NOT NULL - THEN COALESCE( - 'model_call:' || NULLIF(trim(json_extract(${sessionEventsTable.canonicalEventJson}, '$.payload.callId')), ''), - 'run_usage' - ) END`, + THEN COALESCE(json_extract(${sessionEventsTable.canonicalEventJson}, '$.payload.callId'), '') + END`.mapWith((value: string) => { + // Match the ledger's JavaScript trim, including non-space whitespace. + const callId = value.trim(); + return callId.length > 0 ? `model_call:${callId}` : "run_usage"; + }), }) .from(sessionEventsTable) .where(rangeConditions({ ...input, throughSeq })) diff --git a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts index 7d57a7a9..3f9f0a32 100644 --- a/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts +++ b/apps/api/src/modules/vendor-credentials/application/vendor-credential-probe.ts @@ -92,7 +92,7 @@ function sanitizeProviderErrorDetail(detail: string | null | undefined): string ?.trim() .replaceAll(/\s+/gu, " ") .replaceAll(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer ***") - .replaceAll(/\b(sk|rk|pk)-[A-Za-z0-9_*.-]+/gu, "$1-***"); + .replaceAll(/\b((?:sk|rk|pk)-|oc_sk_)[A-Za-z0-9_*.-]+/gu, "$1***"); return normalized === undefined ? undefined : normalized.slice(0, 180); } diff --git a/apps/api/tests/session-process-events.test.ts b/apps/api/tests/session-process-events.test.ts index ff1ae3c4..e092592c 100644 --- a/apps/api/tests/session-process-events.test.ts +++ b/apps/api/tests/session-process-events.test.ts @@ -254,7 +254,9 @@ describe("session process event projection", () => { const database = createProcessEventQueryDatabase(); const snapshots = [ { runId: "run-1", callId: null, tokens: 1916 }, + { runId: "run-1", callId: "\t\n", tokens: 2000 }, { runId: "run-1", callId: null, tokens: 3865 }, + { runId: "run-1", callId: "\t second-call \u00a0", tokens: 5 }, { runId: "run-1", callId: "second-call", tokens: 10 }, { runId: "run-2", callId: null, tokens: 8 }, ]; @@ -275,7 +277,7 @@ describe("session process event projection", () => { projectId: PROJECT_ID, sessionId: SESSION_ID, }); - expect(events.map((event) => event.tokens)).toEqual([null, 3865, 10, 8]); + expect(events.map((event) => event.tokens)).toEqual([null, null, 3865, null, 10, 8]); expect(events.reduce((total, event) => total + (event.tokens ?? 0), 0)).toBe(3883); }); diff --git a/apps/api/tests/vendor-credential-probe.test.ts b/apps/api/tests/vendor-credential-probe.test.ts index 73eeb5e8..a2494b54 100644 --- a/apps/api/tests/vendor-credential-probe.test.ts +++ b/apps/api/tests/vendor-credential-probe.test.ts @@ -4,6 +4,7 @@ import type { PresetModelProtocol } from "@mosoo/contracts/models"; import { readVendorProbeBaseHost, + readVendorProbeErrorCode, toVendorProbeEndpointUrl, validateVendorProbeBaseUrl, vendorProbeModelListIncludes, @@ -11,6 +12,21 @@ import { import { probeVendorCredential } from "../src/modules/vendor-credentials/application/vendor-credential-test"; describe("vendor credential probe", () => { + test.each(["json", "text"] as const)( + "Given an echoed Go key, When reading a %s probe error, Then the key is redacted", + async (format) => { + const message = "Invalid API key oc_sk_test-secret_123; replace sk-legacy-secret"; + const response = + format === "json" + ? Response.json({ error: { message } }, { status: 401 }) + : new Response(message, { status: 401 }); + + expect(await readVendorProbeErrorCode(response)).toBe( + "http_401: Invalid API key oc_sk_***; replace sk-***", + ); + }, + ); + test("builds v1 endpoint URLs from vendor API bases", () => { expect(toVendorProbeEndpointUrl("https://api.example.com", "models")).toBe( "https://api.example.com/v1/models", From 2a4dd9f0776e2259a904cdbde1bb53b0aa42ed49 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Tue, 20 Oct 2026 23:47:06 +0800 Subject: [PATCH 4/8] test(driver): pin deterministic backpressure fixture --- apps/driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/driver b/apps/driver index 49df36e9..e4f6e419 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 49df36e9735f261829ea90b6570382bd33a67775 +Subproject commit e4f6e419d12255fff0b9dc5712a9257260d336a5 From b0d1b7f58ce095e53ffcd6afcae59411f93eca88 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Tue, 20 Oct 2026 23:53:44 +0800 Subject: [PATCH 5/8] test(driver): pin synchronized acp failure fixture --- apps/driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/driver b/apps/driver index e4f6e419..3473fa40 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit e4f6e419d12255fff0b9dc5712a9257260d336a5 +Subproject commit 3473fa404a04bd30a8487e39d043812c09541e61 From e0b650444125f600b9b57d872b58678205cfd6ea Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Tue, 20 Oct 2026 23:55:52 +0800 Subject: [PATCH 6/8] test(driver): pin cancellation child synchronization --- apps/driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/driver b/apps/driver index 3473fa40..29275b86 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 3473fa404a04bd30a8487e39d043812c09541e61 +Subproject commit 29275b86176db8e268c2e19137e1013cd17fa223 From 3b63262477a6f152a02435ba7e5793fe33c719cd Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Wed, 21 Oct 2026 00:02:42 +0800 Subject: [PATCH 7/8] test(driver): pin terminal startup synchronization --- apps/driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/driver b/apps/driver index 29275b86..2b8df25c 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 29275b86176db8e268c2e19137e1013cd17fa223 +Subproject commit 2b8df25c1f560309897e242c5e0d95f804608bf3 From e940702a1218b0535dea36ba3b6afb0735ee827b Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Wed, 21 Oct 2026 00:09:31 +0800 Subject: [PATCH 8/8] test(driver): pin native contract test budget --- apps/driver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/driver b/apps/driver index 2b8df25c..cff74b0d 160000 --- a/apps/driver +++ b/apps/driver @@ -1 +1 @@ -Subproject commit 2b8df25c1f560309897e242c5e0d95f804608bf3 +Subproject commit cff74b0d9fe1ee955d5b1c01640c0615e790e1a0