From 4505210d23015080f3320072533c3b7638cbde6c Mon Sep 17 00:00:00 2001 From: Hanbin Noh <282618027+hanbinnoh@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:38:56 +0900 Subject: [PATCH 01/25] feat: add Command Code OAuth provider --- gui/src/provider-icons.ts | 2 + src/adapters/command-code.ts | 183 +++++++++++++++++++++++++ src/oauth/command-code.ts | 149 ++++++++++++++++++++ src/oauth/index.ts | 24 +++- src/providers/registry.ts | 23 ++++ src/server/adapter-resolve.ts | 3 + tests/command-code-provider.test.ts | 111 +++++++++++++++ tests/provider-registry-parity.test.ts | 2 +- tests/provider-workspace-data.test.ts | 7 + 9 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 src/adapters/command-code.ts create mode 100644 src/oauth/command-code.ts create mode 100644 tests/command-code-provider.test.ts diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 4b1e0871d..10dc59f74 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -63,6 +63,8 @@ const PROVIDER_DISPLAY_NAMES: Record = { "cloudflare-workers-ai": "Cloudflare Workers AI", cline: "Cline", "cline-pass": "ClinePass", + "command-code": "Command Code", + commandcode: "Command Code", nvidia: "NVIDIA NIM", ollama: "Ollama", "ollama-cloud": "Ollama Cloud", diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts new file mode 100644 index 000000000..066cf984e --- /dev/null +++ b/src/adapters/command-code.ts @@ -0,0 +1,183 @@ +import { randomUUID } from "node:crypto"; +import { readdirSync } from "node:fs"; +import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; +import { namespacedToolName } from "../types"; +import type { AdapterRequest, ProviderAdapter } from "./base"; +import type { TranslatorBudget } from "../lib/translator-budget"; + +// Retain the short ids emitted by the first local integration. New requests use the live catalog's +// provider-native IDs directly; this map is compatibility-only and is not a model fallback list. +const COMMAND_CODE_MODEL_ALIASES: Readonly> = { + "deepseek-v4-flash": "deepseek/deepseek-v4-flash", + "kimi-k3": "moonshotai/Kimi-K3", + "glm-5.2": "zai-org/GLM-5.2", +}; + +function textContent(content: string | OcxContentPart[]): string { + return typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join(""); +} + +function wireMessages(messages: OcxMessage[]): Array> { + const out: Array> = []; + for (const message of messages) { + if (message.role === "assistant") { + const content: Array> = []; + for (const part of message.content) { + if (part.type === "text") content.push({ type: "text", text: part.text }); + else if (part.type === "thinking") content.push({ type: "reasoning", text: part.thinking }); + else content.push({ type: "tool-call", toolCallId: part.id, toolName: namespacedToolName(part.namespace, part.name), input: part.arguments }); + } + out.push({ role: "assistant", content }); + continue; + } + if (message.role === "toolResult") { + out.push({ role: "tool", content: [{ + type: "tool-result", + toolCallId: message.toolCallId, + toolName: namespacedToolName(message.toolNamespace, message.toolName), + output: { type: message.isError ? "error-text" : "text", value: textContent(message.content) }, + }] }); + continue; + } + const content: Array> = []; + if (typeof message.content === "string") content.push({ type: "text", text: message.content }); + else for (const part of message.content) { + if (part.type === "text") content.push({ type: "text", text: part.text }); + else content.push({ type: "image", image: part.imageUrl }); + } + out.push({ role: "user", content }); + } + return out; +} + +function wireTools(tools: OcxTool[] | undefined): Array> { + return (tools ?? []).map(tool => ({ + name: namespacedToolName(tool.namespace, tool.name), + description: tool.description, + input_schema: tool.parameters, + })); +} + +function commandCodeConfig(): Record { + let structure: string[] = []; + try { structure = readdirSync(process.cwd()).filter(name => !name.startsWith(".")); } catch { /* cwd may disappear */ } + return { + workingDir: process.cwd(), + date: new Date().toISOString().slice(0, 10), + environment: process.platform, + structure, + isGitRepo: false, + currentBranch: "", + mainBranch: "", + gitStatus: "", + recentCommits: [], + }; +} + +function usage(value: unknown): OcxUsage | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; + const row = value as Record; + const inputTokens = typeof row.inputTokens === "number" ? row.inputTokens : 0; + const outputTokens = typeof row.outputTokens === "number" ? row.outputTokens : 0; + const details = row.inputTokenDetails && typeof row.inputTokenDetails === "object" && !Array.isArray(row.inputTokenDetails) + ? row.inputTokenDetails as Record : {}; + const cachedInputTokens = typeof details.cacheReadTokens === "number" ? details.cacheReadTokens : undefined; + const cacheCreationInputTokens = typeof details.cacheWriteTokens === "number" ? details.cacheWriteTokens : undefined; + return { + inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, + ...(cachedInputTokens !== undefined ? { cachedInputTokens, cacheReadInputTokens: cachedInputTokens } : {}), + ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}), + }; +} + +function eventError(value: unknown): string { + if (typeof value === "string" && value) return value; + if (value && typeof value === "object" && !Array.isArray(value)) { + const message = (value as Record).message; + if (typeof message === "string" && message) return message; + } + return "Command Code stream error"; +} + +async function*ndjson(response: Response): AsyncGenerator> { + if (!response.body) throw new Error("Command Code response body missing"); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + for (;;) { + const { value, done } = await reader.read(); + buffer += decoder.decode(value, { stream: !done }); + let newline = buffer.indexOf("\n"); + while (newline >= 0) { + const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); + if (line) { try { yield JSON.parse(line) as Record; } catch { /* ignore non-events */ } } + newline = buffer.indexOf("\n"); + } + if (done) break; + } + const final = buffer.trim(); + if (final) { try { yield JSON.parse(final) as Record; } catch { /* ignore */ } } +} + +export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter { + return { + name: "command-code", + buildRequest(parsed: OcxParsedRequest): AdapterRequest { + if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code"); + const system = parsed.context.systemPrompt?.join("\n\n") ?? ""; + const body = { + config: commandCodeConfig(), memory: null, taste: null, skills: null, + permissionMode: "standard", mode: "agent", + params: { + model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId, + messages: wireMessages(parsed.context.messages), + tools: wireTools(parsed.context.tools), + system, + max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000, + stream: true, + ...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}), + ...(parsed.options.reasoning && parsed.options.reasoning !== "none" ? { reasoning_effort: parsed.options.reasoning } : {}), + }, + }; + return { + url: `${provider.baseUrl.replace(/\/$/, "")}/alpha/generate`, method: "POST", + headers: { + Authorization: `Bearer ${provider.apiKey}`, + "Content-Type": "application/json", + "User-Agent": "cli", + "x-command-code-version": "1.12.0", + "x-cli-environment": "production", + "x-project-slug": process.cwd().replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(), + "x-taste-learning": "false", + "x-co-flag": "false", + "x-session-id": randomUUID(), + }, + body: JSON.stringify(body), + }; + }, + async *parseStream(response: Response, _budget: TranslatorBudget): AsyncGenerator { + for await (const event of ndjson(response)) { + switch (event.type) { + case "text-delta": if (typeof event.text === "string") yield { type: "text_delta", text: event.text }; break; + case "reasoning-delta": if (typeof event.text === "string") yield { type: "thinking_delta", thinking: event.text }; break; + case "tool-call": { + const id = typeof event.toolCallId === "string" ? event.toolCallId : randomUUID(); + const name = typeof event.toolName === "string" ? event.toolName : "tool"; + const input = event.input ?? event.args ?? {}; + yield { type: "tool_call_start", id, name }; + yield { type: "tool_call_delta", arguments: typeof input === "string" ? input : JSON.stringify(input) }; + yield { type: "tool_call_end" }; + break; + } + case "finish": yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; break; + case "error": yield { type: "error", message: eventError(event.error), status: 502 }; break; + } + } + }, + async parseResponse(response: Response, budget: TranslatorBudget): Promise { + const events: AdapterEvent[] = []; + for await (const event of this.parseStream(response, budget)) events.push(event); + return events; + }, + }; +} diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts new file mode 100644 index 000000000..ecccdb83f --- /dev/null +++ b/src/oauth/command-code.ts @@ -0,0 +1,149 @@ +import type { OAuthController, OAuthCredentials } from "./types"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const COMMAND_CODE_STUDIO_URL = "https://commandcode.ai"; +const COMMAND_CODE_CALLBACK_PORT = 5959; +const LOGIN_TIMEOUT_MS = 120_000; + +interface CommandCodeCallback { + apiKey: string; + state: string; + userId: string; + userName: string; + keyName: string; +} + +interface CommandCodeLocalAuth { + apiKey?: unknown; + userId?: unknown; +} + +export interface CommandCodeLoginOptions { + /** Add-account and reauthentication flows must select a fresh browser identity. */ + importLocal?: "fallback" | "off"; +} + +export function shouldImportLocalCommandCodeAuth(options: CommandCodeLoginOptions = {}): boolean { + return options.importLocal !== "off"; +} + +async function importLocalCommandCodeAuth(): Promise { + let parsed: CommandCodeLocalAuth; + try { + parsed = JSON.parse(await Bun.file(join(homedir(), ".commandcode", "auth.json")).text()) as CommandCodeLocalAuth; + } catch { + return undefined; + } + if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) return undefined; + try { + const response = await fetch("https://api.commandcode.ai/alpha/whoami", { + headers: { Authorization: `Bearer ${parsed.apiKey}`, Accept: "application/json" }, + signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) return undefined; + } catch { + return undefined; + } + return { + access: parsed.apiKey, + refresh: parsed.apiKey, + expires: Number.MAX_SAFE_INTEGER, + ...(typeof parsed.userId === "string" && parsed.userId.length > 0 ? { accountId: parsed.userId } : {}), + source: "local-cli", + }; +} + +function randomState(): string { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Buffer.from(bytes).toString("base64url"); +} + +export function parseCommandCodeCallback(value: unknown, expectedState: string): CommandCodeCallback { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("Command Code callback must be an object"); + } + const body = value as Record; + if (body.state !== expectedState) throw new Error("Command Code OAuth state mismatch"); + for (const field of ["apiKey", "userId", "userName", "keyName"] as const) { + if (typeof body[field] !== "string" || body[field].length === 0) { + throw new Error(`Command Code callback missing ${field}`); + } + } + return body as unknown as CommandCodeCallback; +} + +function createCallbackServer(state: string): { + server: ReturnType; + callback: Promise; +} { + let resolve!: (value: CommandCodeCallback) => void; + const callback = new Promise((res) => { resolve = res; }); + const fetch = async (request: Request): Promise => { + const url = new URL(request.url); + const origin = request.headers.get("origin"); + const headers = new Headers({ + "Content-Type": "application/json", + "Access-Control-Allow-Origin": origin === COMMAND_CODE_STUDIO_URL ? origin : COMMAND_CODE_STUDIO_URL, + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }); + if (request.method === "OPTIONS") return new Response(null, { status: 204, headers }); + if (url.pathname !== "/callback") return Response.json({ success: false, error: "Not found" }, { status: 404, headers }); + if (request.method !== "POST") return Response.json({ success: false, error: "Method not allowed" }, { status: 405, headers }); + try { + const body = await request.json(); + const parsed = parseCommandCodeCallback(body, state); + queueMicrotask(() => resolve(parsed)); + return Response.json({ success: true }, { headers }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return Response.json({ success: false, error: message }, { status: 400, headers }); + } + }; + try { + return { server: Bun.serve({ hostname: "127.0.0.1", port: COMMAND_CODE_CALLBACK_PORT, fetch }), callback }; + } catch { + return { server: Bun.serve({ hostname: "127.0.0.1", port: 0, fetch }), callback }; + } +} + +export async function loginCommandCode(ctrl: OAuthController, options: CommandCodeLoginOptions = {}): Promise { + if (shouldImportLocalCommandCodeAuth(options)) { + const local = await importLocalCommandCodeAuth(); + if (local) { + ctrl.onProgress?.("Imported existing Command Code CLI authentication."); + return local; + } + } + const state = randomState(); + const { server, callback } = createCallbackServer(state); + const callbackUrl = `http://localhost:${server.port}/callback`; + const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`; + ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." }); + ctrl.onProgress?.("Waiting for Command Code authentication..."); + let timeoutId: ReturnType | undefined; + try { + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error("Command Code OAuth callback timed out")), LOGIN_TIMEOUT_MS); + ctrl.signal?.addEventListener("abort", () => { if (timeoutId) clearTimeout(timeoutId); reject(ctrl.signal?.reason); }, { once: true }); + }); + const result = await Promise.race([callback, timeout]); + return { + access: result.apiKey, + refresh: result.apiKey, + expires: Number.MAX_SAFE_INTEGER, + accountId: result.userId, + source: "oauth", + }; + } finally { + if (timeoutId) clearTimeout(timeoutId); + server.stop(true); + } +} + +export async function refreshCommandCodeToken(apiKey: string): Promise { + if (!apiKey) throw new Error("Command Code API key missing; run ocx login command-code"); + return { access: apiKey, refresh: apiKey, expires: Number.MAX_SAFE_INTEGER, source: "oauth" }; +} diff --git a/src/oauth/index.ts b/src/oauth/index.ts index aa2a8278f..6591ec115 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -12,6 +12,7 @@ import { loginChatGPT, refreshChatGPTToken } from "./chatgpt"; import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity"; import { loginCursor, refreshCursorToken } from "./cursor"; import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot"; +import { loginCommandCode, refreshCommandCodeToken } from "./command-code"; import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive"; import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry"; @@ -160,6 +161,14 @@ function oauthDefaultModel(id: string): string { } export const OAUTH_PROVIDERS: Record = { + "command-code": { + // Add-account/reauth must not reimport the current local CLI credential. + login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }), + refresh: refreshCommandCodeToken, + providerConfig: oauthConfig("command-code"), + defaultModel: oauthDefaultModel("command-code"), + defaultRefreshPolicy: "disabled", + }, xai: { // forceLogin skips the local grok-cli import so a SECOND account can be chosen in the browser. login: (ctrl, opts) => loginXai(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }), @@ -722,12 +731,25 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [ const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity"; const GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION = 1 as const; +/** Only migrate the three-model experimental seed; an operator's later `liveModels: false` wins. */ +function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean { + return provider.liveModels === false + && provider.defaultModel === "deepseek-v4-flash" + && JSON.stringify(provider.models) === JSON.stringify(["deepseek-v4-flash", "kimi-k3", "glm-5.2"]); +} + export function reconcileOAuthProviders(config: OcxConfig): boolean { let changed = false; const migrateAntigravityStaticCatalog = config.googleAntigravityStaticCatalogVersion !== GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION; for (const [name, prov] of Object.entries(config.providers)) { const def = OAUTH_PROVIDERS[name]; + if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) { + // The former experimental preset was the exact three-model seed above. It was not a user + // choice to disable discovery, so promote only that shape to the account live catalog. + prov.liveModels = true; + changed = true; + } // Normalize the canonical row before the OAuth-only reconciliation guard. The old GUI and a // manual edit both persist the same bare `true`, with no source metadata, so every ambiguous // pre-marker value is reset once. A deliberate live-discovery choice can be re-enabled after @@ -836,7 +858,7 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void { // reset once; users who deliberately forced discovery can re-enable it after migration. const preserveExistingLiveModels = provider !== GOOGLE_ANTIGRAVITY_PROVIDER || config.googleAntigravityStaticCatalogVersion === GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION; - if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean") { + if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) { next.liveModels = existing.liveModels; } if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) { diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e8fc10dc9..68c6662c5 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -799,6 +799,29 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, noVisionModels: ["grok-build-0.1", "grok-composer-2.5-fast"], }, + { + id: "command-code", + label: "Command Code", + adapter: "command-code", + baseUrl: "https://api.commandcode.ai", + authKind: "oauth", + oauthId: "command-code", + featured: true, + note: "Log in with your Command Code account", + // OAuth needs one initial selection, but the exposed catalog is always discovered from the + // signed-in account. Do not add a static model list here. + defaultModel: "deepseek/deepseek-v4-flash", + liveModels: true, + modelDiscovery: { + url: "https://api.commandcode.ai/provider/v1/models", + maxResponseBytes: 262_144, + maxModels: 256, + }, + // Command Code documents effort support as model-dependent. Do not synthesize a ladder. + reasoningEfforts: [], + defaultMaxOutputTokens: 64_000, + parallelToolCalls: true, + }, { id: "anthropic", label: "Anthropic Claude", diff --git a/src/server/adapter-resolve.ts b/src/server/adapter-resolve.ts index 17680a3da..2edf7e3ee 100644 --- a/src/server/adapter-resolve.ts +++ b/src/server/adapter-resolve.ts @@ -5,6 +5,7 @@ import { createGoogleAdapter } from "../adapters/google"; import { createKiroAdapter } from "../adapters/kiro"; import { createMimoFreeAdapter } from "../adapters/mimo-free"; import { createOpenAIChatAdapter } from "../adapters/openai-chat"; +import { createCommandCodeAdapter } from "../adapters/command-code"; import { createResponsesPassthroughAdapter } from "../adapters/openai-responses"; import type { OcxProviderConfig } from "../types"; import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types"; @@ -57,6 +58,8 @@ export function resolveWireProtocolOverride( /** Build the provider adapter for a resolved provider config. */ export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") { switch (providerConfig.adapter) { + case "command-code": + return createCommandCodeAdapter(providerConfig); case "openai-chat": return createOpenAIChatAdapter(providerConfig); case "anthropic": diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts new file mode 100644 index 000000000..a4cb3d839 --- /dev/null +++ b/tests/command-code-provider.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from "bun:test"; +import { createCommandCodeAdapter } from "../src/adapters/command-code"; +import { parseCommandCodeCallback, shouldImportLocalCommandCodeAuth } from "../src/oauth/command-code"; +import { buildModelsRequest, OAUTH_PROVIDERS } from "../src/oauth"; +import { PROVIDER_REGISTRY } from "../src/providers/registry"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { createTestTranslatorBudget } from "./helpers/translator-budget"; + +const provider: OcxProviderConfig = { + adapter: "command-code", + baseUrl: "https://api.commandcode.ai", + authMode: "oauth", + apiKey: "secret-command-key", + defaultMaxOutputTokens: 64_000, +}; + +function parsed(modelId = "kimi-k3"): OcxParsedRequest { + return { + modelId, + stream: true, + context: { + systemPrompt: ["system"], + messages: [{ role: "user", content: "hello", timestamp: 1 }], + tools: [{ name: "lookup", description: "lookup", parameters: { type: "object" } }], + }, + options: { reasoning: "high", maxOutputTokens: 100 }, + }; +} + +describe("Command Code provider", () => { + test("registry and OAuth surfaces stay in parity", () => { + const registry = PROVIDER_REGISTRY.find(row => row.id === "command-code"); + expect(registry).toMatchObject({ + adapter: "command-code", + authKind: "oauth", + defaultModel: "deepseek/deepseek-v4-flash", + liveModels: true, + modelDiscovery: { + url: "https://api.commandcode.ai/provider/v1/models", + maxResponseBytes: 262_144, + maxModels: 256, + }, + }); + expect(registry?.models).toBeUndefined(); + expect(OAUTH_PROVIDERS["command-code"]?.providerConfig).toMatchObject({ + adapter: "command-code", + baseUrl: "https://api.commandcode.ai", + authMode: "oauth", + }); + }); + + test("validates callback shape and state without exposing the key", () => { + expect(parseCommandCodeCallback({ apiKey: "key", state: "state", userId: "u", userName: "name", keyName: "cli" }, "state")).toMatchObject({ userId: "u" }); + expect(() => parseCommandCodeCallback({ apiKey: "key", state: "wrong", userId: "u", userName: "name", keyName: "cli" }, "state")).toThrow("state mismatch"); + }); + + test("uses live account discovery and only imports local CLI auth for the first account", () => { + const request = buildModelsRequest(provider, "secret-command-key", "command-code"); + expect(request).toEqual({ + url: "https://api.commandcode.ai/provider/v1/models", + headers: { Authorization: "Bearer secret-command-key" }, + }); + expect(shouldImportLocalCommandCodeAuth()).toBe(true); + expect(shouldImportLocalCommandCodeAuth({ importLocal: "off" })).toBe(false); + }); + + test("builds the proprietary generate request with canonical model and bearer auth", () => { + const request = createCommandCodeAdapter(provider).buildRequest(parsed()); + expect(request).not.toBeInstanceOf(Promise); + const built = request as Exclude>; + const body = JSON.parse(built.body); + expect(built.url).toBe("https://api.commandcode.ai/alpha/generate"); + expect(built.headers.Authorization).toBe("Bearer secret-command-key"); + expect(body.params).toMatchObject({ model: "moonshotai/Kimi-K3", reasoning_effort: "high", max_tokens: 100, stream: true }); + expect(body.params.tools[0]).toMatchObject({ name: "lookup" }); + expect(built.body).not.toContain("secret-command-key"); + }); + + test("passes every canonical Command Code id through unchanged", () => { + const request = createCommandCodeAdapter(provider).buildRequest(parsed("xai/grok-4.5")); + expect(request).not.toBeInstanceOf(Promise); + const built = request as Exclude>; + expect(JSON.parse(built.body).params.model).toBe("xai/grok-4.5"); + }); + + test("omits effort when the caller did not choose one", () => { + const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed("claude-haiku-4-5"), options: { maxOutputTokens: 100 } }); + expect(request).not.toBeInstanceOf(Promise); + const built = request as Exclude>; + expect(JSON.parse(built.body).params).not.toHaveProperty("reasoning_effort"); + }); + + test("parses NDJSON text, reasoning, tools, usage, and finish", async () => { + const response = new Response([ + JSON.stringify({ type: "reasoning-delta", text: "think" }), + JSON.stringify({ type: "text-delta", text: "hello" }), + JSON.stringify({ type: "tool-call", toolCallId: "call_1", toolName: "lookup", input: { q: "x" } }), + JSON.stringify({ type: "finish", rawFinishReason: "tool_use", totalUsage: { inputTokens: 10, outputTokens: 4, inputTokenDetails: { cacheReadTokens: 6, cacheWriteTokens: 2 } } }), + ].join("\n")); + const events = []; + for await (const event of createCommandCodeAdapter(provider).parseStream(response, createTestTranslatorBudget())) events.push(event); + expect(events).toEqual([ + { type: "thinking_delta", thinking: "think" }, + { type: "text_delta", text: "hello" }, + { type: "tool_call_start", id: "call_1", name: "lookup" }, + { type: "tool_call_delta", arguments: '{"q":"x"}' }, + { type: "tool_call_end" }, + { type: "done", usage: { inputTokens: 10, outputTokens: 4, totalTokens: 14, cachedInputTokens: 6, cacheReadInputTokens: 6, cacheCreationInputTokens: 2 }, stopReason: "tool_use" }, + ]); + }); +}); diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index cb332e24f..ef51082a6 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -671,7 +671,7 @@ describe("provider registry parity", () => { test("GUI preset projection preserves current featured set plus key catalog and custom", () => { const featured = deriveFeaturedProviderIds(); expect(featured).toEqual([ - "openai", "xai", "anthropic", "anthropic-apikey", "kimi", "openai-apikey", "umans", "opencode-go", "openrouter", + "openai", "xai", "command-code", "anthropic", "anthropic-apikey", "kimi", "openai-apikey", "umans", "opencode-go", "openrouter", "groq", "google", "azure-openai", "ollama", "vllm", "lm-studio", "opencode-free", "mimo-free", ]); diff --git a/tests/provider-workspace-data.test.ts b/tests/provider-workspace-data.test.ts index 11d7a43de..5e3f98184 100644 --- a/tests/provider-workspace-data.test.ts +++ b/tests/provider-workspace-data.test.ts @@ -447,6 +447,13 @@ describe("provider-icons", () => { expect(formatProviderDisplayName("chatgpt", englishT)).toBe("ChatGPT"); }); + test("Command Code account and API-key presets share the catalog display name", () => { + expect(formatProviderDisplayName("command-code", englishT)).toBe("Command Code"); + expect(formatProviderDisplayName("commandcode", englishT)).toBe("Command Code"); + expect(isCatalogProviderId("command-code")).toBe(true); + expect(isCatalogProviderId("commandcode")).toBe(true); + }); + test("unknown simple ids are title-cased; mixedCase custom names pass through", () => { expect(formatProviderDisplayName("my-proxy", englishT)).toBe("My Proxy"); expect(formatProviderDisplayName("MyProxy", englishT)).toBe("MyProxy"); From 4b95fdb0aa709a00a69ead6360fe01d4bc2da545 Mon Sep 17 00:00:00 2001 From: Hanbin Noh <282618027+hanbinnoh@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:22:39 +0900 Subject: [PATCH 02/25] fix: harden Command Code capabilities --- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/provider-icons.ts | 4 +- src/adapters/command-code.ts | 217 +++++++++++++++++++++----- src/oauth/command-code.ts | 5 +- src/providers/command-code-efforts.ts | 79 ++++++++++ src/providers/registry.ts | 8 +- tests/command-code-provider.test.ts | 57 ++++++- 12 files changed, 328 insertions(+), 48 deletions(-) create mode 100644 src/providers/command-code-efforts.ts diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 46d78face..97c204ba2 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -37,6 +37,7 @@ export const de: Record = { "theme.dark": "Dunkel", "theme.system": "System", "lang.label": "Sprache", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark Coding-Tarif", "provider.name.volcengineAgentPlan": "Volcengine Ark Agent-Tarif", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 8389a237a..a6f1e054e 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -44,6 +44,7 @@ export const en = { "theme.dark": "Dark", "theme.system": "System", "lang.label": "Language", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan", "provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 77aaa0dd8..499d7e735 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -42,6 +42,7 @@ export const ja: Record = { "theme.dark": "ダーク", "theme.system": "システム", "lang.label": "言語", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark コーディングプラン", "provider.name.volcengineAgentPlan": "Volcengine Ark エージェントプラン", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 93e03755e..c94838288 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -37,6 +37,7 @@ export const ko: Record = { "theme.dark": "다크", "theme.system": "시스템", "lang.label": "언어", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark 코딩 플랜", "provider.name.volcengineAgentPlan": "Volcengine Ark 에이전트 플랜", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 48e9149b6..4194febad 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -42,6 +42,7 @@ export const ru: Record = { "theme.dark": "Тёмная", "theme.system": "Системная", "lang.label": "Язык", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark — тариф Coding", "provider.name.volcengineAgentPlan": "Volcengine Ark — тариф Agent", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 15e6053b3..7b471dbfd 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -37,6 +37,7 @@ export const zh: Record = { "theme.dark": "深色", "theme.system": "跟随系统", "lang.label": "语言", + "provider.name.commandCode": "Command Code", "provider.name.volcengine": "火山方舟", "provider.name.volcengineCodingPlan": "火山方舟编程套餐", "provider.name.volcengineAgentPlan": "火山方舟智能体套餐", diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 10dc59f74..60a8da30e 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -63,8 +63,6 @@ const PROVIDER_DISPLAY_NAMES: Record = { "cloudflare-workers-ai": "Cloudflare Workers AI", cline: "Cline", "cline-pass": "ClinePass", - "command-code": "Command Code", - commandcode: "Command Code", nvidia: "NVIDIA NIM", ollama: "Ollama", "ollama-cloud": "Ollama Cloud", @@ -101,6 +99,8 @@ const PROVIDER_DISPLAY_NAMES: Record = { }; const PROVIDER_DISPLAY_NAME_KEYS: Record = { + "command-code": "provider.name.commandCode", + commandcode: "provider.name.commandCode", volcengine: "provider.name.volcengine", "volcengine-coding-plan": "provider.name.volcengineCodingPlan", "volcengine-agent-plan": "provider.name.volcengineAgentPlan", diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 066cf984e..50382edb8 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,9 +1,14 @@ import { randomUUID } from "node:crypto"; import { readdirSync } from "node:fs"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; -import { namespacedToolName } from "../types"; -import type { AdapterRequest, ProviderAdapter } from "./base"; +import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types"; +import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; import type { TranslatorBudget } from "../lib/translator-budget"; +import { readBoundedResponseBody } from "../lib/bounded-body"; +import { configuredReasoningEfforts } from "../reasoning-effort"; +import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts"; +import { identifyRoutedModel } from "./identity"; +import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge"; // Retain the short ids emitted by the first local integration. New requests use the live catalog's // provider-native IDs directly; this map is compatibility-only and is not a model fallback list. @@ -50,19 +55,50 @@ function wireMessages(messages: OcxMessage[]): Array> { return out; } -function wireTools(tools: OcxTool[] | undefined): Array> { - return (tools ?? []).map(tool => ({ +function visibleTools(parsed: OcxParsedRequest): OcxTool[] { + const choice = parsed.options.toolChoice; + if (choice === "none") return []; + const tools = parsed.context.tools ?? []; + if (isAllowedToolChoice(choice)) { + const allowed = new Set(choice.allowedTools); + return tools.filter(tool => toolAllowedByChoice(tool, allowed)); + } + if (choice && typeof choice !== "string") { + return tools.filter(tool => tool.name === choice.name || namespacedToolName(tool.namespace, tool.name) === choice.name); + } + return tools; +} + +function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined { + const choice = parsed.options.toolChoice; + if (choice === "required" || (isAllowedToolChoice(choice) && choice.mode === "required")) { + return "Tool choice is required for this turn. Make at least one call from the advertised tool catalog before answering."; + } + if (choice && typeof choice !== "string" && !isAllowedToolChoice(choice)) { + return `Tool choice is required for this turn. Call the advertised tool named ${namespacedToolName(undefined, choice.name)} before answering.`; + } + return undefined; +} + +function wireTools(tools: OcxTool[]): Array> { + return tools.map(tool => ({ name: namespacedToolName(tool.namespace, tool.name), description: tool.description, input_schema: tool.parameters, })); } -function commandCodeConfig(): Record { +function currentWorkingDirectory(): string | undefined { + try { return process.cwd(); } catch { return undefined; } +} + +function commandCodeConfig(cwd: string | undefined): Record { let structure: string[] = []; - try { structure = readdirSync(process.cwd()).filter(name => !name.startsWith(".")); } catch { /* cwd may disappear */ } + if (cwd) { + try { structure = readdirSync(cwd).filter(name => !name.startsWith(".")); } catch { /* workspace metadata is optional */ } + } return { - workingDir: process.cwd(), + ...(cwd ? { workingDir: cwd } : {}), date: new Date().toISOString().slice(0, 10), environment: process.platform, structure, @@ -99,64 +135,161 @@ function eventError(value: unknown): string { return "Command Code stream error"; } -async function*ndjson(response: Response): AsyncGenerator> { +async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenerator> { if (!response.body) throw new Error("Command Code response body missing"); const reader = response.body.getReader(); const decoder = new TextDecoder(); + const encoder = new TextEncoder(); let buffer = ""; - for (;;) { - const { value, done } = await reader.read(); - buffer += decoder.decode(value, { stream: !done }); - let newline = buffer.indexOf("\n"); - while (newline >= 0) { - const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); - if (line) { try { yield JSON.parse(line) as Record; } catch { /* ignore non-events */ } } - newline = buffer.indexOf("\n"); + let bufferBytes = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + const next = buffer + decoder.decode(value, { stream: !done }); + const nextBytes = encoder.encode(next).byteLength; + const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" }); + buffer = next; + reservation.commitRetained(); + budget.releaseRetained(bufferBytes, { kind: "live_transient" }); + bufferBytes = nextBytes; + let newline = buffer.indexOf("\n"); + while (newline >= 0) { + const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); + if (line) { try { yield JSON.parse(line) as Record; } catch { /* ignore non-events */ } } + newline = buffer.indexOf("\n"); + } + const residualBytes = encoder.encode(buffer).byteLength; + const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" }); + residualReservation.commitRetained(); + budget.releaseRetained(bufferBytes, { kind: "live_transient" }); + bufferBytes = residualBytes; + if (done) break; } - if (done) break; + const final = buffer.trim(); + if (final) { try { yield JSON.parse(final) as Record; } catch { /* ignore */ } } + } finally { + budget.releaseRetained(bufferBytes, { kind: "live_transient" }); + reader.releaseLock(); } - const final = buffer.trim(); - if (final) { try { yield JSON.parse(final) as Record; } catch { /* ignore */ } } +} + +function isReasoningEffortRejection(status: number, payload: string): boolean { + return (status === 400 || status === 422) && /reasoning[_ -]?effort|unsupported effort|invalid effort/i.test(payload); +} + +function requestWithoutReasoningEffort(request: AdapterRequest): AdapterRequest | undefined { + try { + const body = JSON.parse(request.body) as { params?: Record }; + if (!body.params?.reasoning_effort) return undefined; + delete body.params.reasoning_effort; + return { ...request, body: JSON.stringify(body), reasoningLog: undefined }; + } catch { + return undefined; + } +} + +async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContext | undefined, executor: typeof globalThis.fetch): Promise { + const timeout = new AbortController(); + const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000); + const callerSignal = ctx?.abortSignal ?? new AbortController().signal; + try { + return await executor(request.url, { + method: request.method, + headers: request.headers, + body: request.body, + redirect: "manual", + signal: AbortSignal.any([callerSignal, timeout.signal]), + }); + } finally { + clearTimeout(timer); + } +} + +function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined { + if (!requested || requested === "none") return undefined; + const supported = commandCodeReasoningEfforts(modelId) ?? configuredReasoningEfforts(provider, modelId); + if (!supported) return undefined; + // Command Code's official profiles describe xhigh as the CLI label that maps to + // the wire value `max`; preserve that mapping without advertising a synthetic tier. + const wire = requested === "xhigh" && supported.includes("max") ? "max" : requested; + return supported.includes(wire) ? wire : undefined; } export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter { + const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch; return { name: "command-code", buildRequest(parsed: OcxParsedRequest): AdapterRequest { if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code"); - const system = parsed.context.systemPrompt?.join("\n\n") ?? ""; + const cwd = currentWorkingDirectory(); + const tools = visibleTools(parsed); + const toolNudge = buildNonOpenAIToolCatalogNudgeForTools(tools, parsed.options.toolChoice); + const choiceInstruction = toolChoiceInstruction(parsed); + const system = identifyRoutedModel([ + ...(parsed.context.systemPrompt ?? []), + ...(toolNudge ? [toolNudge] : []), + ...(choiceInstruction ? [choiceInstruction] : []), + ].join("\n\n"), parsed.modelId); + const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning); const body = { - config: commandCodeConfig(), memory: null, taste: null, skills: null, + config: commandCodeConfig(cwd), memory: null, taste: null, skills: null, permissionMode: "standard", mode: "agent", params: { model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId, messages: wireMessages(parsed.context.messages), - tools: wireTools(parsed.context.tools), + tools: wireTools(tools), system, max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000, stream: true, ...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}), - ...(parsed.options.reasoning && parsed.options.reasoning !== "none" ? { reasoning_effort: parsed.options.reasoning } : {}), + ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), }, }; + const headers: Record = { + Authorization: `Bearer ${provider.apiKey}`, + "Content-Type": "application/json", + "User-Agent": "cli", + "x-command-code-version": "1.12.0", + "x-cli-environment": "production", + "x-taste-learning": "false", + "x-co-flag": "false", + "x-session-id": randomUUID(), + }; + if (cwd) headers["x-project-slug"] = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(); return { url: `${provider.baseUrl.replace(/\/$/, "")}/alpha/generate`, method: "POST", - headers: { - Authorization: `Bearer ${provider.apiKey}`, - "Content-Type": "application/json", - "User-Agent": "cli", - "x-command-code-version": "1.12.0", - "x-cli-environment": "production", - "x-project-slug": process.cwd().replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(), - "x-taste-learning": "false", - "x-co-flag": "false", - "x-session-id": randomUUID(), - }, + headers, body: JSON.stringify(body), + ...(reasoningEffort ? { reasoningLog: { effectiveEffort: reasoningEffort, wireField: "reasoning_effort" as const, wireValue: reasoningEffort } } : {}), }; }, - async *parseStream(response: Response, _budget: TranslatorBudget): AsyncGenerator { - for await (const event of ndjson(response)) { + async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise { + const response = await fetchCommandCode(request, ctx, executor); + if (response.ok) return response; + const currentEffort = (() => { + try { return (JSON.parse(request.body) as { params?: { reasoning_effort?: unknown } }).params?.reasoning_effort; } catch { return undefined; } + })(); + if (typeof currentEffort !== "string") return response; + let body = ""; + try { + const observed = await readBoundedResponseBody(response.clone(), { signal: ctx?.abortSignal, maxBytes: 8 * 1024 }); + if (!observed.displaySafe) return response; + body = observed.text; + } catch { return response; } + if (!isReasoningEffortRejection(response.status, body)) return response; + const modelId = (() => { + try { return (JSON.parse(request.body) as { params?: { model?: unknown } }).params?.model; } catch { return undefined; } + })(); + if (typeof modelId !== "string") return response; + const refreshed = await refreshCommandCodeReasoningEfforts(modelId, executor); + if (!refreshed || refreshed.includes(currentEffort)) return response; + const retry = requestWithoutReasoningEffort(request); + if (!retry) return response; + try { void response.body?.cancel(); } catch { /* already closed */ } + return fetchCommandCode(retry, ctx, executor); + }, + async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator { + for await (const event of ndjson(response, budget)) { switch (event.type) { case "text-delta": if (typeof event.text === "string") yield { type: "text_delta", text: event.text }; break; case "reasoning-delta": if (typeof event.text === "string") yield { type: "thinking_delta", thinking: event.text }; break; @@ -164,9 +297,17 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA const id = typeof event.toolCallId === "string" ? event.toolCallId : randomUUID(); const name = typeof event.toolName === "string" ? event.toolName : "tool"; const input = event.input ?? event.args ?? {}; + const argumentsText = typeof input === "string" ? input : JSON.stringify(input); yield { type: "tool_call_start", id, name }; - yield { type: "tool_call_delta", arguments: typeof input === "string" ? input : JSON.stringify(input) }; - yield { type: "tool_call_end" }; + budget.openCall(id); + try { + const reservation = budget.reserveTransient(new TextEncoder().encode(argumentsText).byteLength, { kind: "tool_args", callId: id }); + reservation.commitRetained(); + yield { type: "tool_call_delta", arguments: argumentsText }; + yield { type: "tool_call_end" }; + } finally { + budget.closeCall(id); + } break; } case "finish": yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; break; diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index ecccdb83f..be63db202 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -110,6 +110,9 @@ function createCallbackServer(state: string): { } export async function loginCommandCode(ctrl: OAuthController, options: CommandCodeLoginOptions = {}): Promise { + if (ctrl.signal?.aborted) { + throw ctrl.signal.reason ?? new DOMException("Command Code login aborted", "AbortError"); + } if (shouldImportLocalCommandCodeAuth(options)) { const local = await importLocalCommandCodeAuth(); if (local) { @@ -119,7 +122,7 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo } const state = randomState(); const { server, callback } = createCallbackServer(state); - const callbackUrl = `http://localhost:${server.port}/callback`; + const callbackUrl = `http://127.0.0.1:${server.port}/callback`; const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`; ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." }); ctrl.onProgress?.("Waiting for Command Code authentication..."); diff --git a/src/providers/command-code-efforts.ts b/src/providers/command-code-efforts.ts new file mode 100644 index 000000000..c7709a4b5 --- /dev/null +++ b/src/providers/command-code-efforts.ts @@ -0,0 +1,79 @@ +const COMMAND_CODE_MODEL_EFFORTS = { + "deepseek/deepseek-v4-pro": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/deepseek-v4-pro", + }, + "deepseek/deepseek-v4-flash": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/deepseek-v4-flash", + }, + "zai-org/glm-5.2": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/glm-5-2", + }, +} as const; + +/** + * Official Command Code model-profile facts, not a model catalog. Models remain + * account-scoped and come exclusively from the authenticated /provider/v1/models endpoint. + */ +export const COMMAND_CODE_MODEL_REASONING_EFFORTS: Record = Object.fromEntries( + Object.entries(COMMAND_CODE_MODEL_EFFORTS).map(([id, row]) => [id, [...row.efforts]]), +); + +const refreshedEfforts = new Map(); + +function keyFor(modelId: string): string { + return modelId.trim().toLowerCase(); +} + +export function commandCodeReasoningEfforts(modelId: string): readonly string[] | undefined { + const key = keyFor(modelId); + return refreshedEfforts.get(key) ?? COMMAND_CODE_MODEL_REASONING_EFFORTS[key]; +} + +function parsedProfileEfforts(page: string): string[] | undefined { + const match = page.match(/Reasoning efforts\s+([^.;]+?)\s+are supported;\s*([^.]*)/i); + if (!match) return undefined; + const listed = match[1]!.toLowerCase().match(/\b(?:low|medium|high|xhigh|max)\b/g) ?? []; + const mapped = match[2]!.toLowerCase().match(/\b(?:low|medium|high|xhigh|max)\s+maps to\s+(?:low|medium|high|xhigh|max)\b/g) ?? []; + const normalized = new Set(listed); + for (const mapping of mapped) { + const [, source, target] = mapping.match(/(low|medium|high|xhigh|max)\s+maps to\s+(low|medium|high|xhigh|max)/) ?? []; + if (source && target) { + normalized.delete(source); + normalized.add(target); + } + } + return normalized.size > 0 ? [...normalized] : []; +} + +/** + * Refresh one stale effort record only after the upstream rejects an effort request. + * A failed or unparseable public profile deliberately leaves the known table unchanged. + */ +export async function refreshCommandCodeReasoningEfforts( + modelId: string, + fetchFn: typeof globalThis.fetch = globalThis.fetch, +): Promise { + const key = keyFor(modelId); + const profile = COMMAND_CODE_MODEL_EFFORTS[key as keyof typeof COMMAND_CODE_MODEL_EFFORTS]; + if (!profile) return undefined; + try { + const response = await fetchFn(profile.profileUrl, { + headers: { Accept: "text/html" }, + signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) return undefined; + const efforts = parsedProfileEfforts(await response.text()); + if (efforts === undefined) return undefined; + refreshedEfforts.set(key, efforts); + return efforts; + } catch { + return undefined; + } +} + +export function resetCommandCodeReasoningEffortsForTest(): void { + refreshedEfforts.clear(); +} diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 68c6662c5..c82d19c54 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -14,6 +14,7 @@ import { cursorModelInputModalities, cursorModelReasoningEfforts, } from "../adapters/cursor/discovery"; +import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts"; export type ProviderAuthKind = "forward" | "oauth" | "key" | "local"; export type MetadataModelIdNormalize = "case-insensitive"; @@ -817,10 +818,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ maxResponseBytes: 262_144, maxModels: 256, }, - // Command Code documents effort support as model-dependent. Do not synthesize a ladder. + // These are capability facts from official Command Code model profiles, not seeded models. + // Unknown/new live models deliberately do not advertise a reasoning picker. reasoningEfforts: [], + modelReasoningEfforts: COMMAND_CODE_MODEL_REASONING_EFFORTS, defaultMaxOutputTokens: 64_000, - parallelToolCalls: true, + // The proprietary generate wire has no verified per-request serialization flag. + parallelToolCalls: false, }, { id: "anthropic", diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index a4cb3d839..7f802e75e 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { createCommandCodeAdapter } from "../src/adapters/command-code"; -import { parseCommandCodeCallback, shouldImportLocalCommandCodeAuth } from "../src/oauth/command-code"; +import { loginCommandCode, parseCommandCodeCallback, shouldImportLocalCommandCodeAuth } from "../src/oauth/command-code"; import { buildModelsRequest, OAUTH_PROVIDERS } from "../src/oauth"; +import { commandCodeReasoningEfforts, resetCommandCodeReasoningEffortsForTest } from "../src/providers/command-code-efforts"; import { PROVIDER_REGISTRY } from "../src/providers/registry"; import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; import { createTestTranslatorBudget } from "./helpers/translator-budget"; @@ -14,7 +15,7 @@ const provider: OcxProviderConfig = { defaultMaxOutputTokens: 64_000, }; -function parsed(modelId = "kimi-k3"): OcxParsedRequest { +function parsed(modelId = "deepseek/deepseek-v4-flash"): OcxParsedRequest { return { modelId, stream: true, @@ -27,6 +28,8 @@ function parsed(modelId = "kimi-k3"): OcxParsedRequest { }; } +afterEach(() => resetCommandCodeReasoningEffortsForTest()); + describe("Command Code provider", () => { test("registry and OAuth surfaces stay in parity", () => { const registry = PROVIDER_REGISTRY.find(row => row.id === "command-code"); @@ -42,6 +45,10 @@ describe("Command Code provider", () => { }, }); expect(registry?.models).toBeUndefined(); + expect(registry?.modelReasoningEfforts).toMatchObject({ + "deepseek/deepseek-v4-flash": ["high", "max"], + "zai-org/glm-5.2": ["high", "max"], + }); expect(OAUTH_PROVIDERS["command-code"]?.providerConfig).toMatchObject({ adapter: "command-code", baseUrl: "https://api.commandcode.ai", @@ -54,6 +61,12 @@ describe("Command Code provider", () => { expect(() => parseCommandCodeCallback({ apiKey: "key", state: "wrong", userId: "u", userName: "name", keyName: "cli" }, "state")).toThrow("state mismatch"); }); + test("rejects an already-aborted login before it creates a callback server", async () => { + const controller = new AbortController(); + controller.abort(new Error("cancelled before login")); + await expect(loginCommandCode({ signal: controller.signal }, { importLocal: "off" })).rejects.toThrow("cancelled before login"); + }); + test("uses live account discovery and only imports local CLI auth for the first account", () => { const request = buildModelsRequest(provider, "secret-command-key", "command-code"); expect(request).toEqual({ @@ -64,14 +77,14 @@ describe("Command Code provider", () => { expect(shouldImportLocalCommandCodeAuth({ importLocal: "off" })).toBe(false); }); - test("builds the proprietary generate request with canonical model and bearer auth", () => { + test("builds the proprietary generate request with an officially supported effort and bearer auth", () => { const request = createCommandCodeAdapter(provider).buildRequest(parsed()); expect(request).not.toBeInstanceOf(Promise); const built = request as Exclude>; const body = JSON.parse(built.body); expect(built.url).toBe("https://api.commandcode.ai/alpha/generate"); expect(built.headers.Authorization).toBe("Bearer secret-command-key"); - expect(body.params).toMatchObject({ model: "moonshotai/Kimi-K3", reasoning_effort: "high", max_tokens: 100, stream: true }); + expect(body.params).toMatchObject({ model: "deepseek/deepseek-v4-flash", reasoning_effort: "high", max_tokens: 100, stream: true }); expect(body.params.tools[0]).toMatchObject({ name: "lookup" }); expect(built.body).not.toContain("secret-command-key"); }); @@ -83,6 +96,40 @@ describe("Command Code provider", () => { expect(JSON.parse(built.body).params.model).toBe("xai/grok-4.5"); }); + test("does not advertise an unverified effort for models absent from the official table", () => { + const request = createCommandCodeAdapter(provider).buildRequest(parsed("moonshotai/Kimi-K3")); + expect(request).not.toBeInstanceOf(Promise); + expect(JSON.parse((request as Exclude>).body).params).not.toHaveProperty("reasoning_effort"); + }); + + test("filters tool declarations when tool_choice disables tools", () => { + const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed(), options: { toolChoice: "none" } }); + expect(request).not.toBeInstanceOf(Promise); + expect(JSON.parse((request as Exclude>).body).params.tools).toEqual([]); + }); + + test("refreshes a stale official effort record only after a reasoning rejection and retries without it", async () => { + const requests: Array<{ url: string; body?: string }> = []; + const fetch = (async (url: string | URL | Request, init?: RequestInit) => { + const href = String(url); + requests.push({ url: href, body: typeof init?.body === "string" ? init.body : undefined }); + if (href.includes("commandcode.ai/models/")) { + return new Response("Reasoning efforts high are supported; no other reasoning settings."); + } + return requests.filter(request => request.url.endsWith("/alpha/generate")).length === 1 + ? new Response(JSON.stringify({ error: "unsupported reasoning_effort" }), { status: 400 }) + : new Response("{}", { status: 200 }); + }) as typeof globalThis.fetch; + const adapter = createCommandCodeAdapter({ ...provider, fetch } as OcxProviderConfig); + const request = adapter.buildRequest({ ...parsed(), options: { reasoning: "max" } }); + expect(request).not.toBeInstanceOf(Promise); + const response = await adapter.fetchResponse!(request as Exclude>); + expect(response.ok).toBe(true); + expect(commandCodeReasoningEfforts("deepseek/deepseek-v4-flash")).toEqual(["high"]); + const generated = requests.filter(request => request.url.endsWith("/alpha/generate")); + expect(JSON.parse(generated[1]!.body!).params).not.toHaveProperty("reasoning_effort"); + }); + test("omits effort when the caller did not choose one", () => { const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed("claude-haiku-4-5"), options: { maxOutputTokens: 100 } }); expect(request).not.toBeInstanceOf(Promise); From b551d297a051e2bff3f1b2f15679a963bef93e27 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:34:38 +0200 Subject: [PATCH 03/25] fix(command-code): address review findings on OAuth, images, and metadata - Preserve tool-result image parts as [image] markers instead of silently dropping them (contentPartsToText), matching other adapters. - Bind the OAuth callback on both IPv4 and IPv6 loopback and race the shared manual-paste fallback (raw API key or pasted callback JSON/URL), matching OAuthCallbackFlow behavior for Windows/headless/remote cases. - Minimize workspace metadata: cap the directory listing at 64 entries, bound the x-project-slug header, and drop the always-empty git stubs. - Update providers docs (EN + ja/ko/ru/zh) and the registry note to describe OAuth login with local CLI credential import. Co-authored-by: CommandCodeBot --- .../src/content/docs/guides/providers.md | 7 +- .../src/content/docs/ja/guides/providers.md | 7 +- .../src/content/docs/ko/guides/providers.md | 8 +- .../src/content/docs/ru/guides/providers.md | 9 ++- .../content/docs/zh-cn/guides/providers.md | 6 +- src/adapters/command-code.ts | 28 +++---- src/oauth/command-code.ts | 77 +++++++++++++++++-- src/providers/registry.ts | 2 +- tests/command-code-provider.test.ts | 62 +++++++++++++++ 9 files changed, 167 insertions(+), 39 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index b4a9573ae..269e81fad 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -303,9 +303,10 @@ are out of scope. Create keys at [Hyperbolic](https://app.hyperbolic.ai). **Command Code discovery.** The preset reads Command Code's public `/provider/v1/models` list from the fixed Provider API host, preserves provider-native ids, and caps discovery at 256 KiB and 256 raw -rows. The model catalog is unauthenticated, so the CLI login flow reports the key as unverifiable -instead of a false positive. Chat requests use the configured Bearer key; API access requires the -Provider plan, and CLI auth bridging for Go/Pro subscriptions is not yet available. Create keys at +rows. `ocx login command-code` supports OAuth via browser sign-in (with optional local CLI credential +import from `~/.commandcode/auth.json` for existing Command Code CLI users); the model catalog is +account-scoped and comes from the authenticated discovery endpoint after login. Chat requests use the +configured Bearer key. Create keys at [Command Code Studio](https://commandcode.ai/studio/). > **Baseten scope:** The preset covers Baseten's shared [Model APIs](https://docs.baseten.co/inference/model-apis/overview) diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 48f04235c..d02f2657f 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -224,9 +224,10 @@ vision-language chat のみを対象とし、別系統の image、audio、GPU en **Command Code の discovery:** preset は Command Code の公開 `/provider/v1/models` リストを固定の Provider API ホストから読み、スラッシュを含むネイティブモデル ID を保持し、live discovery を -256 KiB と raw 256 行に制限します。モデルカタログは未認証のため、CLI ログインフローはキーを -誤って有効と報告せず、検証不能として報告します。チャットリクエストは設定済みの bearer キーを使います。 -API アクセスには Provider プランが必要で、Go/Pro サブスクリプション向けの CLI 認証ブリッジはまだ利用できません。 +256 KiB と raw 256 行に制限します。`ocx login command-code` はブラウザーでの OAuth サインインを +サポートします(既存の Command Code CLI ユーザー向けに `~/.commandcode/auth.json` からのローカル +CLI 資格情報の取り込みも可能)。モデルカタログはアカウント単位で、ログイン後に認証済みの +discovery エンドポイントから取得します。チャットリクエストは設定済みの bearer キーを使います。 キーは [Command Code Studio](https://commandcode.ai/studio/) で作成します。 > **Baseten の対象範囲:** このプリセットは Baseten の共有 [Model APIs](https://docs.baseten.co/inference/model-apis/overview) diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index ae2973499..b86ac9713 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -223,10 +223,10 @@ Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목 **Command Code 검색:** 프리셋은 Command Code의 공개 `/provider/v1/models` 목록을 고정된 Provider API 호스트에서 읽고, 슬래시가 포함된 네이티브 모델 ID를 보존하며 live discovery를 256 KiB와 raw 행 -256개로 제한합니다. 모델 카탈로그는 인증이 없으므로 CLI 로그인 흐름은 키를 유효하다고 잘못 보고하지 -않고 검증 불가로 보고합니다. 채팅 요청은 설정된 bearer 키를 사용하며, API 액세스에는 Provider 플랜이 -필요하고 Go/Pro 구독자용 CLI 인증 브리지는 아직 제공되지 않습니다. 키는 -[Command Code Studio](https://commandcode.ai/studio/)에서 생성합니다. +256개로 제한합니다. `ocx login command-code`는 브라우저 OAuth 로그인을 지원하며(기존 Command Code +CLI 사용자는 `~/.commandcode/auth.json`의 로컬 CLI 자격 증명을 가져올 수 있음), 모델 카탈로그는 +계정 단위이며 로그인 후 인증된 discovery 엔드포인트에서 가져옵니다. 채팅 요청은 설정된 bearer +키를 사용합니다. 키는 [Command Code Studio](https://commandcode.ai/studio/)에서 생성합니다. > **Baseten 범위:** 이 프리셋은 Baseten의 공유 [Model APIs](https://docs.baseten.co/inference/model-apis/overview)만 > 지원합니다. 로컬 사용에는 개인 [API 키](https://docs.baseten.co/organization/api-keys)를, 공유/프로덕션 diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 4f912949a..98e181a95 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -236,10 +236,11 @@ endpoint в него не входят. Ключи создаются в [Hyperb **Discovery для Command Code.** Пресет читает публичный список `/provider/v1/models` с фиксированного хоста Provider API, сохраняет нативные id моделей со знаком `/` и ограничивает live discovery размером -256 KiB и 256 исходными строками. Каталог моделей не требует аутентификации, поэтому CLI-флоу входа -сообщает ключ как непроверенный, а не как ложноположительно действительный. Запросы чата используют -настроенный bearer-ключ; для доступа к API требуется план Provider, а CLI-мост аутентификации для -подписок Go/Pro пока недоступен. Ключи создаются в [Command Code Studio](https://commandcode.ai/studio/). +256 KiB и 256 исходными строками. `ocx login command-code` поддерживает вход через OAuth в браузере +(с возможностью импорта локальных учётных данных CLI из `~/.commandcode/auth.json` для существующих +пользователей CLI Command Code); каталог моделей привязан к учётной записи и берётся из +аутентифицированного discovery endpoint после входа. Запросы чата используют настроенный bearer-ключ. +Ключи создаются в [Command Code Studio](https://commandcode.ai/studio/). > **Область Baseten:** пресет поддерживает только общие [Model APIs](https://docs.baseten.co/inference/model-apis/overview) > Baseten. Для локальной работы используйте личный [API-ключ](https://docs.baseten.co/organization/api-keys), 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 e8ce7f5d1..468a71e95 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -207,9 +207,9 @@ image、audio 和 GPU 端点不在范围内。密钥可在 [Hyperbolic](https:// **Command Code 发现:**该预设从固定的 Provider API 主机读取 Command Code 公开的 `/provider/v1/models` 列表,保留含 `/` 的原生模型 id,并将实时发现限制为 256 KiB 和 256 条原始记录。 -模型目录无需认证,因此 CLI 登录流程会将密钥报告为无法验证,而不是误报为有效。聊天请求使用已配置的 -bearer 密钥;API 访问需要 Provider 套餐,Go/Pro 订阅用户的 CLI 认证桥接尚不可用。 -密钥可在 [Command Code Studio](https://commandcode.ai/studio/) 创建。 +`ocx login command-code` 支持通过浏览器进行 OAuth 登录(现有 Command Code CLI 用户还可选择从 +`~/.commandcode/auth.json` 导入本地 CLI 凭据);模型目录按账户隔离,并在登录后从经过认证的发现 +端点获取。聊天请求使用已配置的 bearer 密钥。密钥可在 [Command Code Studio](https://commandcode.ai/studio/) 创建。 > **Baseten 范围:**该预设仅覆盖 Baseten 的共享 [Model APIs](https://docs.baseten.co/inference/model-apis/overview)。 > 本地使用可选择个人 [API 密钥](https://docs.baseten.co/organization/api-keys);共享或生产用途请使用具备 diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 50382edb8..b85ffa007 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { readdirSync } from "node:fs"; -import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; +import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types"; import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; import type { TranslatorBudget } from "../lib/translator-budget"; @@ -9,6 +9,7 @@ import { configuredReasoningEfforts } from "../reasoning-effort"; import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts"; import { identifyRoutedModel } from "./identity"; import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge"; +import { contentPartsToText } from "./image"; // Retain the short ids emitted by the first local integration. New requests use the live catalog's // provider-native IDs directly; this map is compatibility-only and is not a model fallback list. @@ -18,10 +19,6 @@ const COMMAND_CODE_MODEL_ALIASES: Readonly> = { "glm-5.2": "zai-org/GLM-5.2", }; -function textContent(content: string | OcxContentPart[]): string { - return typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join(""); -} - function wireMessages(messages: OcxMessage[]): Array> { const out: Array> = []; for (const message of messages) { @@ -40,7 +37,7 @@ function wireMessages(messages: OcxMessage[]): Array> { type: "tool-result", toolCallId: message.toolCallId, toolName: namespacedToolName(message.toolNamespace, message.toolName), - output: { type: message.isError ? "error-text" : "text", value: textContent(message.content) }, + output: { type: message.isError ? "error-text" : "text", value: contentPartsToText(message.content) }, }] }); continue; } @@ -92,21 +89,26 @@ function currentWorkingDirectory(): string | undefined { try { return process.cwd(); } catch { return undefined; } } +/** Cap the workspace listing so a large directory does not ship every entry name upstream. */ +const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64; + +/** Derive a bounded project slug from the working directory for the `x-project-slug` header. */ +function projectSlug(cwd: string): string { + return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace"; +} + function commandCodeConfig(cwd: string | undefined): Record { let structure: string[] = []; if (cwd) { - try { structure = readdirSync(cwd).filter(name => !name.startsWith(".")); } catch { /* workspace metadata is optional */ } + try { + structure = readdirSync(cwd).filter(name => !name.startsWith(".")).slice(0, MAX_WORKSPACE_STRUCTURE_ENTRIES); + } catch { /* workspace metadata is optional */ } } return { ...(cwd ? { workingDir: cwd } : {}), date: new Date().toISOString().slice(0, 10), environment: process.platform, structure, - isGitRepo: false, - currentBranch: "", - mainBranch: "", - gitStatus: "", - recentCommits: [], }; } @@ -255,7 +257,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA "x-co-flag": "false", "x-session-id": randomUUID(), }; - if (cwd) headers["x-project-slug"] = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(); + if (cwd) headers["x-project-slug"] = projectSlug(cwd); return { url: `${provider.baseUrl.replace(/\/$/, "")}/alpha/generate`, method: "POST", headers, diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index be63db202..aec908452 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -1,10 +1,13 @@ import type { OAuthController, OAuthCredentials } from "./types"; import { homedir } from "node:os"; import { join } from "node:path"; +import { isAddrInUse } from "../server/ports"; +import { parseCallbackInput } from "./callback-server"; const COMMAND_CODE_STUDIO_URL = "https://commandcode.ai"; const COMMAND_CODE_CALLBACK_PORT = 5959; const LOGIN_TIMEOUT_MS = 120_000; +const CALLBACK_PATH = "/callback"; interface CommandCodeCallback { apiKey: string; @@ -75,7 +78,7 @@ export function parseCommandCodeCallback(value: unknown, expectedState: string): } function createCallbackServer(state: string): { - server: ReturnType; + servers: Array>; callback: Promise; } { let resolve!: (value: CommandCodeCallback) => void; @@ -90,7 +93,7 @@ function createCallbackServer(state: string): { "Access-Control-Allow-Headers": "Content-Type", }); if (request.method === "OPTIONS") return new Response(null, { status: 204, headers }); - if (url.pathname !== "/callback") return Response.json({ success: false, error: "Not found" }, { status: 404, headers }); + if (url.pathname !== CALLBACK_PATH) return Response.json({ success: false, error: "Not found" }, { status: 404, headers }); if (request.method !== "POST") return Response.json({ success: false, error: "Method not allowed" }, { status: 405, headers }); try { const body = await request.json(); @@ -102,11 +105,56 @@ function createCallbackServer(state: string): { return Response.json({ success: false, error: message }, { status: 400, headers }); } }; + const create = (port: number): Array> => { + // The advertised callback host is `127.0.0.1`; on Windows `localhost` commonly resolves to `::1` + // first, so also bind the IPv6 loopback best-effort (mirrors the shared OAuthCallbackFlow). + const servers = [Bun.serve({ hostname: "127.0.0.1", port, fetch })]; + try { + servers.push(Bun.serve({ hostname: "::1", port: servers[0].port, fetch })); + } catch (error) { + if (isAddrInUse(error)) { + for (const server of servers) server.stop(true); + throw error; + } + // IPv6 unsupported (EAFNOSUPPORT etc.) degrades to the IPv4-only listener. + } + return servers; + }; try { - return { server: Bun.serve({ hostname: "127.0.0.1", port: COMMAND_CODE_CALLBACK_PORT, fetch }), callback }; + return { servers: create(COMMAND_CODE_CALLBACK_PORT), callback }; } catch { - return { server: Bun.serve({ hostname: "127.0.0.1", port: 0, fetch }), callback }; + return { servers: create(0), callback }; + } +} + +/** Validate a raw pasted Command Code API key against the fixed Provider API host. */ +async function validatePastedApiKey(apiKey: string): Promise { + try { + const response = await fetch("https://api.commandcode.ai/alpha/whoami", { + headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, + signal: AbortSignal.timeout(10_000), + }); + return response.ok; + } catch { + return false; + } +} + +/** Manual-paste fallback: a raw API key, or a pasted callback JSON/URL that carries `apiKey`. */ +function parsePastedCommandCodeInput(input: string, expectedState: string): CommandCodeCallback | undefined { + const trimmed = input.trim(); + if (!trimmed) return undefined; + if (trimmed.startsWith("{")) { + try { + return parseCommandCodeCallback(JSON.parse(trimmed) as unknown, expectedState); + } catch { + return undefined; + } } + const parsed = parseCallbackInput(trimmed); + const apiKey = parsed.code?.trim(); + if (!apiKey) return undefined; + return { apiKey, state: expectedState, userId: "", userName: "", keyName: "manual" }; } export async function loginCommandCode(ctrl: OAuthController, options: CommandCodeLoginOptions = {}): Promise { @@ -121,8 +169,8 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo } } const state = randomState(); - const { server, callback } = createCallbackServer(state); - const callbackUrl = `http://127.0.0.1:${server.port}/callback`; + const { servers, callback } = createCallbackServer(state); + const callbackUrl = `http://127.0.0.1:${servers[0].port}${CALLBACK_PATH}`; const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`; ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." }); ctrl.onProgress?.("Waiting for Command Code authentication..."); @@ -132,7 +180,20 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo timeoutId = setTimeout(() => reject(new Error("Command Code OAuth callback timed out")), LOGIN_TIMEOUT_MS); ctrl.signal?.addEventListener("abort", () => { if (timeoutId) clearTimeout(timeoutId); reject(ctrl.signal?.reason); }, { once: true }); }); - const result = await Promise.race([callback, timeout]); + const manual = ctrl.onManualCodeInput + ? (async (): Promise => { + while (true) { + // The loop keeps waiting until a valid paste arrives; invalid pastes re-prompt. + // `callback`/`timeout` are the only paths that settle the outer race first. + const input = await ctrl.onManualCodeInput?.(state); + if (input === undefined) continue; + const pasted = parsePastedCommandCodeInput(input, state); + if (pasted && (await validatePastedApiKey(pasted.apiKey))) return pasted; + } + })() + : undefined; + const result = await Promise.race([callback, timeout, ...(manual ? [manual] : [])]); + if (result === undefined) throw new Error("Command Code OAuth callback cancelled"); return { access: result.apiKey, refresh: result.apiKey, @@ -142,7 +203,7 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo }; } finally { if (timeoutId) clearTimeout(timeoutId); - server.stop(true); + for (const server of servers) server.stop(true); } } diff --git a/src/providers/registry.ts b/src/providers/registry.ts index c82d19c54..ffd135074 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1305,7 +1305,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, // Verified 2026-08-03: public /provider/v1/models returns 51 rows; /chat/completions returns // 401 UNAUTHORIZED without a Bearer key. Primary source: https://commandcode.ai/docs/provider. - note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. CLI auth bridging for Go/Pro subscriptions is not yet available. Docs: https://commandcode.ai/docs/provider.", + note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. Use `ocx login command-code` for OAuth account login (imports an existing local Command Code CLI credential when present). Docs: https://commandcode.ai/docs/provider.", }, // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" }, diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 7f802e75e..c6f17b904 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -67,6 +67,30 @@ describe("Command Code provider", () => { await expect(loginCommandCode({ signal: controller.signal }, { importLocal: "off" })).rejects.toThrow("cancelled before login"); }); + test("accepts a manually pasted API key when the browser callback cannot reach the loopback server", async () => { + const controller = new AbortController(); + const originalFetch = globalThis.fetch; + const calls: string[] = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const href = String(input); + calls.push(href); + if (href.includes("whoami")) return new Response(JSON.stringify({ ok: true }), { status: 200 }); + throw new Error(`unexpected fetch: ${href}`); + }) as typeof globalThis.fetch; + try { + const credentials = await loginCommandCode({ + onAuth: () => {}, + onProgress: () => {}, + onManualCodeInput: async () => "sk-pasted-key", + signal: controller.signal, + }, { importLocal: "off" }); + expect(credentials).toMatchObject({ access: "sk-pasted-key", source: "oauth" }); + expect(calls.some(href => href.includes("whoami"))).toBe(true); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("uses live account discovery and only imports local CLI auth for the first account", () => { const request = buildModelsRequest(provider, "secret-command-key", "command-code"); expect(request).toEqual({ @@ -96,6 +120,44 @@ describe("Command Code provider", () => { expect(JSON.parse(built.body).params.model).toBe("xai/grok-4.5"); }); + test("preserves tool-result text and marks image parts instead of dropping them", () => { + const image = "data:image/png;base64,AAAA"; + const request = createCommandCodeAdapter(provider).buildRequest({ + ...parsed(), + context: { + ...parsed().context, + messages: [{ + role: "toolResult", + toolCallId: "call_1", + toolName: "view_image", + content: [{ type: "text", text: "screenshot:" }, { type: "image", imageUrl: image }], + isError: false, + timestamp: 1, + }], + }, + }); + expect(request).not.toBeInstanceOf(Promise); + const built = request as Exclude>; + const body = JSON.parse(built.body); + expect(body.params.messages[0]).toMatchObject({ + role: "tool", + content: [{ type: "tool-result", output: { type: "text", value: "screenshot:[image]" } }], + }); + expect(built.body).not.toContain(image); + }); + + test("keeps the generate config to workspace metadata and bounds the project slug", () => { + const request = createCommandCodeAdapter(provider).buildRequest(parsed()); + expect(request).not.toBeInstanceOf(Promise); + const built = request as Exclude>; + const body = JSON.parse(built.body); + expect(body.config).not.toHaveProperty("isGitRepo"); + expect(body.config).not.toHaveProperty("recentCommits"); + expect(body.config.structure).toBeInstanceOf(Array); + expect(typeof body.config.workingDir).toBe("string"); + expect(built.headers["x-project-slug"]?.length ?? 0).toBeLessThanOrEqual(64); + }); + test("does not advertise an unverified effort for models absent from the official table", () => { const request = createCommandCodeAdapter(provider).buildRequest(parsed("moonshotai/Kimi-K3")); expect(request).not.toBeInstanceOf(Promise); From 711be1331765191bb57bf6c4de0621b041622f1b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:04:20 +0200 Subject: [PATCH 04/25] fix(command-code): address CodeRabbit findings on images, abort, and streaming - Carry tool-result image parts in a follow-up user message (the proprietary tool-result output is text-only), so view_image data reaches the model instead of being dropped or flattened to a marker. - Honor ctrl.signal during local CLI credential import (abort during whoami). - Always stop callback servers even when controller callbacks throw. - Bound the model-profile page read with readBoundedResponseBody. - Cancel the NDJSON reader on teardown, send parsed.stream as the wire stream field, and emit a fallback done when a stream ends without a finish event. - Add error-event, fallback-done, and stream-field tests; assert callback keys never leak into thrown errors. - Fix the remaining "public catalog / unverifiable" claims in providers docs (EN + ja/ko/ru/zh) so they match the authenticated discovery wording. Co-authored-by: CommandCodeBot --- .../src/content/docs/guides/providers.md | 5 +- .../src/content/docs/ja/guides/providers.md | 5 +- .../src/content/docs/ko/guides/providers.md | 5 +- .../src/content/docs/ru/guides/providers.md | 6 +-- .../content/docs/zh-cn/guides/providers.md | 4 +- src/adapters/command-code.ts | 28 +++++++++-- src/oauth/command-code.ts | 26 ++++++---- src/providers/command-code-efforts.ts | 8 +++- tests/command-code-provider.test.ts | 48 +++++++++++++++---- 9 files changed, 97 insertions(+), 38 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 269e81fad..75da9ab01 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -218,8 +218,7 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses opencodex ships 69 built-in presets: 58 key-based, seven OAuth, three local, and one default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, -validates the key, and stores it; validation is provider-specific, and Command Code's public -catalog reports keys as unverifiable. Notable entries: +validates the key, and stores it; validation is provider-specific. Notable entries: **ClinePass** uses a Cline API key with the [official subscription catalog](https://docs.cline.bot/getting-started/clinepass) and [Chat Completions endpoint](https://docs.cline.bot/api/chat-completions), operated by Cline Bot Inc. under @@ -301,7 +300,7 @@ slash-containing native model ids, and caps live discovery at 256 KiB and 256 ra serverless text and vision-language chat only; Hyperbolic's separate image, audio, and GPU endpoints are out of scope. Create keys at [Hyperbolic](https://app.hyperbolic.ai). -**Command Code discovery.** The preset reads Command Code's public `/provider/v1/models` list from +**Command Code discovery.** The preset reads Command Code's `/provider/v1/models` list from the fixed Provider API host, preserves provider-native ids, and caps discovery at 256 KiB and 256 raw rows. `ocx login command-code` supports OAuth via browser sign-in (with optional local CLI credential import from `~/.commandcode/auth.json` for existing Command Code CLI users); the model catalog is diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index d02f2657f..f36aaeb72 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -145,8 +145,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht opencodex には組み込みプリセットが 69 個含まれています。キー方式 58、OAuth 7、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 -入力したキーを検証した後保存します(検証はプロバイダー固有で、Command Code の公開カタログはキーを -検証不能として報告します)。主な項目は以下のとおりです: +入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: **ClinePass** は Cline API キーで[公式サブスクリプションカタログ](https://docs.cline.bot/getting-started/clinepass)と [Chat Completions エンドポイント](https://docs.cline.bot/api/chat-completions)に接続します。運営主体は @@ -222,7 +221,7 @@ Volcengine Agent Plan は `openai-responses` アダプターでネイティブ R vision-language chat のみを対象とし、別系統の image、audio、GPU endpoint は対象外です。キーは [Hyperbolic](https://app.hyperbolic.ai) で作成します。 -**Command Code の discovery:** preset は Command Code の公開 `/provider/v1/models` リストを固定の +**Command Code の discovery:** preset は Command Code の `/provider/v1/models` リストを固定の Provider API ホストから読み、スラッシュを含むネイティブモデル ID を保持し、live discovery を 256 KiB と raw 256 行に制限します。`ocx login command-code` はブラウザーでの OAuth サインインを サポートします(既存の Command Code CLI ユーザー向けに `~/.commandcode/auth.json` からのローカル diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index b86ac9713..7276b36ff 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -144,8 +144,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http opencodex에는 빌트인 프리셋이 69개 들어 있습니다. 키 방식 58개, OAuth 7개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, -입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다르며, Command Code의 공개 카탈로그는 키를 -검증 불가로 보고합니다). 주요 항목은 다음과 같습니다: +입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: **ClinePass**는 Cline API 키로 [공식 구독 카탈로그](https://docs.cline.bot/getting-started/clinepass)와 [Chat Completions 엔드포인트](https://docs.cline.bot/api/chat-completions)에 연결합니다. 운영 주체는 @@ -221,7 +220,7 @@ Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목 대상으로 하며 별도 image, audio, GPU 엔드포인트는 범위에서 제외합니다. 키는 [Hyperbolic](https://app.hyperbolic.ai)에서 생성합니다. -**Command Code 검색:** 프리셋은 Command Code의 공개 `/provider/v1/models` 목록을 고정된 Provider API +**Command Code 검색:** 프리셋은 Command Code의 `/provider/v1/models` 목록을 고정된 Provider API 호스트에서 읽고, 슬래시가 포함된 네이티브 모델 ID를 보존하며 live discovery를 256 KiB와 raw 행 256개로 제한합니다. `ocx login command-code`는 브라우저 OAuth 로그인을 지원하며(기존 Command Code CLI 사용자는 `~/.commandcode/auth.json`의 로컬 CLI 자격 증명을 가져올 수 있음), 모델 카탈로그는 diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 98e181a95..2a5216f27 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -155,8 +155,8 @@ OAuth-провайдеры, чьи учётные данные содержат opencodex поставляется с 69 встроенными пресетами: 58 на основе ключей, семь OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу -выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера, а публичный -каталог Command Code сообщает ключ как непроверенный. Наиболее заметные записи: +выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. +Наиболее заметные записи: **ClinePass** подключается с помощью Cline API key к [официальному каталогу подписки](https://docs.cline.bot/getting-started/clinepass) и [Chat Completions endpoint](https://docs.cline.bot/api/chat-completions). Оператор — Cline Bot Inc., указанный в @@ -234,7 +234,7 @@ Volcengine Agent Plan использует нативную конечную т строками. Он охватывает только serverless text и vision-language chat; отдельные image, audio и GPU endpoint в него не входят. Ключи создаются в [Hyperbolic](https://app.hyperbolic.ai). -**Discovery для Command Code.** Пресет читает публичный список `/provider/v1/models` с фиксированного +**Discovery для Command Code.** Пресет читает список `/provider/v1/models` с фиксированного хоста Provider API, сохраняет нативные id моделей со знаком `/` и ограничивает live discovery размером 256 KiB и 256 исходными строками. `ocx login command-code` поддерживает вход через OAuth в браузере (с возможностью импорта локальных учётных данных CLI из `~/.commandcode/auth.json` для существующих 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 468a71e95..cd983e129 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -133,7 +133,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal opencodex 内置 69 个预设:58 个密钥预设、7 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 -验证因提供商而异,Command Code 的公开目录会将密钥报告为无法验证。主要条目包括: +验证因提供商而异。主要条目包括: **ClinePass** 使用 Cline API 密钥连接[官方订阅目录](https://docs.cline.bot/getting-started/clinepass)和 [Chat Completions 端点](https://docs.cline.bot/api/chat-completions)。运营主体是 @@ -205,7 +205,7 @@ OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 UR 并将实时发现限制为 256 KiB 和 256 条原始记录。它仅覆盖 serverless text 与 vision-language chat;独立的 image、audio 和 GPU 端点不在范围内。密钥可在 [Hyperbolic](https://app.hyperbolic.ai) 创建。 -**Command Code 发现:**该预设从固定的 Provider API 主机读取 Command Code 公开的 +**Command Code 发现:**该预设从固定的 Provider API 主机读取 Command Code 的 `/provider/v1/models` 列表,保留含 `/` 的原生模型 id,并将实时发现限制为 256 KiB 和 256 条原始记录。 `ocx login command-code` 支持通过浏览器进行 OAuth 登录(现有 Command Code CLI 用户还可选择从 `~/.commandcode/auth.json` 导入本地 CLI 凭据);模型目录按账户隔离,并在登录后从经过认证的发现 diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index b85ffa007..187651776 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { readdirSync } from "node:fs"; -import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; +import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types"; import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; import type { TranslatorBudget } from "../lib/translator-budget"; @@ -9,7 +9,6 @@ import { configuredReasoningEfforts } from "../reasoning-effort"; import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts"; import { identifyRoutedModel } from "./identity"; import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge"; -import { contentPartsToText } from "./image"; // Retain the short ids emitted by the first local integration. New requests use the live catalog's // provider-native IDs directly; this map is compatibility-only and is not a model fallback list. @@ -19,6 +18,10 @@ const COMMAND_CODE_MODEL_ALIASES: Readonly> = { "glm-5.2": "zai-org/GLM-5.2", }; +function toolResultText(content: string | OcxContentPart[]): string { + return typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join(""); +} + function wireMessages(messages: OcxMessage[]): Array> { const out: Array> = []; for (const message of messages) { @@ -37,8 +40,15 @@ function wireMessages(messages: OcxMessage[]): Array> { type: "tool-result", toolCallId: message.toolCallId, toolName: namespacedToolName(message.toolNamespace, message.toolName), - output: { type: message.isError ? "error-text" : "text", value: contentPartsToText(message.content) }, + output: { type: message.isError ? "error-text" : "text", value: toolResultText(message.content) }, }] }); + // The proprietary wire's tool-result output is text-only; image parts returned by a + // tool (e.g. Codex view_image) cannot live inside it. Carry them in a follow-up user + // message using the same image encoding as the user branch so the bytes reach the model. + const images = typeof message.content === "string" ? [] : message.content.filter(part => part.type === "image"); + if (images.length > 0) { + out.push({ role: "user", content: images.map(part => ({ type: "image", image: (part as { imageUrl: string }).imageUrl })) }); + } continue; } const content: Array> = []; @@ -171,6 +181,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera if (final) { try { yield JSON.parse(final) as Record; } catch { /* ignore */ } } } finally { budget.releaseRetained(bufferBytes, { kind: "live_transient" }); + try { await reader.cancel(); } catch { /* already closed */ } reader.releaseLock(); } } @@ -242,7 +253,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA tools: wireTools(tools), system, max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000, - stream: true, + stream: parsed.stream, ...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}), ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), }, @@ -291,6 +302,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA return fetchCommandCode(retry, ctx, executor); }, async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator { + let sawFinish = false; for await (const event of ndjson(response, budget)) { switch (event.type) { case "text-delta": if (typeof event.text === "string") yield { type: "text_delta", text: event.text }; break; @@ -312,10 +324,16 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA } break; } - case "finish": yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; break; + case "finish": + sawFinish = true; + yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; + break; case "error": yield { type: "error", message: eventError(event.error), status: 502 }; break; } } + // A stream that ends without a finish event still needs a terminal done so the + // server does not wait on an adapter that silently stopped emitting. + if (!sawFinish) yield { type: "done", usage: undefined, stopReason: undefined }; }, async parseResponse(response: Response, budget: TranslatorBudget): Promise { const events: AdapterEvent[] = []; diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index aec908452..263eb67c5 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -31,7 +31,10 @@ export function shouldImportLocalCommandCodeAuth(options: CommandCodeLoginOption return options.importLocal !== "off"; } -async function importLocalCommandCodeAuth(): Promise { +async function importLocalCommandCodeAuth(signal?: AbortSignal): Promise { + if (signal?.aborted) { + throw signal.reason ?? new DOMException("Command Code login aborted", "AbortError"); + } let parsed: CommandCodeLocalAuth; try { parsed = JSON.parse(await Bun.file(join(homedir(), ".commandcode", "auth.json")).text()) as CommandCodeLocalAuth; @@ -42,10 +45,11 @@ async function importLocalCommandCodeAuth(): Promise> = []; let timeoutId: ReturnType | undefined; try { + const created = createCallbackServer(state); + servers = created.servers; + const callbackUrl = `http://127.0.0.1:${servers[0].port}${CALLBACK_PATH}`; + const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`; + ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." }); + ctrl.onProgress?.("Waiting for Command Code authentication..."); const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error("Command Code OAuth callback timed out")), LOGIN_TIMEOUT_MS); ctrl.signal?.addEventListener("abort", () => { if (timeoutId) clearTimeout(timeoutId); reject(ctrl.signal?.reason); }, { once: true }); @@ -192,7 +198,7 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo } })() : undefined; - const result = await Promise.race([callback, timeout, ...(manual ? [manual] : [])]); + const result = await Promise.race([created.callback, timeout, ...(manual ? [manual] : [])]); if (result === undefined) throw new Error("Command Code OAuth callback cancelled"); return { access: result.apiKey, diff --git a/src/providers/command-code-efforts.ts b/src/providers/command-code-efforts.ts index c7709a4b5..0f2b88eb9 100644 --- a/src/providers/command-code-efforts.ts +++ b/src/providers/command-code-efforts.ts @@ -1,3 +1,5 @@ +import { readBoundedResponseBody } from "../lib/bounded-body"; + const COMMAND_CODE_MODEL_EFFORTS = { "deepseek/deepseek-v4-pro": { efforts: ["high", "max"], @@ -65,7 +67,11 @@ export async function refreshCommandCodeReasoningEfforts( signal: AbortSignal.timeout(10_000), }); if (!response.ok) return undefined; - const efforts = parsedProfileEfforts(await response.text()); + // Bound the profile page before parsing: a large or malformed page must not + // allocate unbounded memory on the request path. + const observed = await readBoundedResponseBody(response, { maxBytes: 256 * 1024 }); + if (!observed.displaySafe) return undefined; + const efforts = parsedProfileEfforts(observed.text); if (efforts === undefined) return undefined; refreshedEfforts.set(key, efforts); return efforts; diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index c6f17b904..3ccdb0679 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -57,8 +57,15 @@ describe("Command Code provider", () => { }); test("validates callback shape and state without exposing the key", () => { - expect(parseCommandCodeCallback({ apiKey: "key", state: "state", userId: "u", userName: "name", keyName: "cli" }, "state")).toMatchObject({ userId: "u" }); - expect(() => parseCommandCodeCallback({ apiKey: "key", state: "wrong", userId: "u", userName: "name", keyName: "cli" }, "state")).toThrow("state mismatch"); + const secret = "super-secret-callback-key"; + const parsedCallback = parseCommandCodeCallback({ apiKey: secret, state: "state", userId: "u", userName: "name", keyName: "cli" }, "state"); + expect(parsedCallback).toMatchObject({ userId: "u" }); + let thrown = ""; + try { + parseCommandCodeCallback({ apiKey: secret, state: "wrong", userId: "u", userName: "name", keyName: "cli" }, "state"); + } catch (error) { thrown = String(error); } + expect(thrown).toContain("state mismatch"); + expect(thrown).not.toContain(secret); }); test("rejects an already-aborted login before it creates a callback server", async () => { @@ -120,7 +127,7 @@ describe("Command Code provider", () => { expect(JSON.parse(built.body).params.model).toBe("xai/grok-4.5"); }); - test("preserves tool-result text and marks image parts instead of dropping them", () => { + test("carries tool-result images in a follow-up user message instead of dropping them", () => { const image = "data:image/png;base64,AAAA"; const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed(), @@ -139,11 +146,10 @@ describe("Command Code provider", () => { expect(request).not.toBeInstanceOf(Promise); const built = request as Exclude>; const body = JSON.parse(built.body); - expect(body.params.messages[0]).toMatchObject({ - role: "tool", - content: [{ type: "tool-result", output: { type: "text", value: "screenshot:[image]" } }], - }); - expect(built.body).not.toContain(image); + expect(body.params.messages).toEqual([ + { role: "tool", content: [{ type: "tool-result", toolCallId: "call_1", toolName: "view_image", output: { type: "text", value: "screenshot:" } }] }, + { role: "user", content: [{ type: "image", image }] }, + ]); }); test("keeps the generate config to workspace metadata and bounds the project slug", () => { @@ -217,4 +223,30 @@ describe("Command Code provider", () => { { type: "done", usage: { inputTokens: 10, outputTokens: 4, totalTokens: 14, cachedInputTokens: 6, cacheReadInputTokens: 6, cacheCreationInputTokens: 2 }, stopReason: "tool_use" }, ]); }); + + test("yields an error event for upstream error events", async () => { + const response = new Response(JSON.stringify({ type: "error", error: { message: "upstream boom" } })); + const events = []; + for await (const event of createCommandCodeAdapter(provider).parseStream(response, createTestTranslatorBudget())) events.push(event); + expect(events).toEqual([ + { type: "error", message: "upstream boom", status: 502 }, + { type: "done", usage: undefined, stopReason: undefined }, + ]); + }); + + test("emits a fallback done when the stream ends without a finish event", async () => { + const response = new Response(JSON.stringify({ type: "text-delta", text: "partial" })); + const events = []; + for await (const event of createCommandCodeAdapter(provider).parseStream(response, createTestTranslatorBudget())) events.push(event); + expect(events).toEqual([ + { type: "text_delta", text: "partial" }, + { type: "done", usage: undefined, stopReason: undefined }, + ]); + }); + + test("sends parsed.stream as the wire stream field", () => { + const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed(), stream: false }); + expect(request).not.toBeInstanceOf(Promise); + expect(JSON.parse((request as Exclude>).body).params.stream).toBe(false); + }); }); From 175c68cf545c865c937ad3b8631eb340cd131965 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:16:58 +0200 Subject: [PATCH 05/25] fix(command-code): restore upstream-required git config fields with real values Live testing against the Command Code /alpha/generate endpoint surfaced a 400: the upstream schema requires config.isGitRepo, currentBranch, mainBranch, gitStatus, and recentCommits. Restore them populated from the actual workspace git state (bounded: 8 commits, 2 KiB status, 2 s timeout, fail-safe fallback) instead of dropping them, and keep the structure/slug caps. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 37 +++++++++++++++++++++++++++++ tests/command-code-provider.test.ts | 10 +++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 187651776..50b80a25b 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { execFileSync } from "node:child_process"; import { readdirSync } from "node:fs"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types"; @@ -101,12 +102,46 @@ function currentWorkingDirectory(): string | undefined { /** Cap the workspace listing so a large directory does not ship every entry name upstream. */ const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64; +/** Cap how many recent commit subjects the config carries. */ +const MAX_RECENT_COMMITS = 8; +/** Cap the git status text sent upstream. */ +const MAX_GIT_STATUS_LENGTH = 2048; /** Derive a bounded project slug from the working directory for the `x-project-slug` header. */ function projectSlug(cwd: string): string { return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace"; } +interface GitWorkspaceInfo { + isGitRepo: boolean; + currentBranch: string; + mainBranch: string; + gitStatus: string; + recentCommits: string[]; +} + +/** Best-effort git metadata for the upstream config contract; every read fails safe. */ +function gitWorkspaceInfo(cwd: string | undefined): GitWorkspaceInfo { + const fallback: GitWorkspaceInfo = { isGitRepo: false, currentBranch: "", mainBranch: "", gitStatus: "", recentCommits: [] }; + if (!cwd) return fallback; + const run = (args: string[]): string => { + try { + return execFileSync("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }).trim(); + } catch { + return ""; + } + }; + const root = run(["rev-parse", "--show-toplevel"]); + if (!root) return fallback; + return { + isGitRepo: true, + currentBranch: run(["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD", + mainBranch: run(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])?.replace(/^origin\//, "") || run(["rev-parse", "--abbrev-ref", "HEAD"]) || "", + gitStatus: run(["status", "--porcelain"]).slice(0, MAX_GIT_STATUS_LENGTH), + recentCommits: run(["log", "--oneline", `-${MAX_RECENT_COMMITS}`]).split("\n").filter(Boolean).slice(0, MAX_RECENT_COMMITS), + }; +} + function commandCodeConfig(cwd: string | undefined): Record { let structure: string[] = []; if (cwd) { @@ -114,11 +149,13 @@ function commandCodeConfig(cwd: string | undefined): Record { structure = readdirSync(cwd).filter(name => !name.startsWith(".")).slice(0, MAX_WORKSPACE_STRUCTURE_ENTRIES); } catch { /* workspace metadata is optional */ } } + const git = gitWorkspaceInfo(cwd); return { ...(cwd ? { workingDir: cwd } : {}), date: new Date().toISOString().slice(0, 10), environment: process.platform, structure, + ...git, }; } diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 3ccdb0679..a601cd89f 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -152,13 +152,17 @@ describe("Command Code provider", () => { ]); }); - test("keeps the generate config to workspace metadata and bounds the project slug", () => { + test("keeps the generate config to bounded workspace and git metadata", () => { const request = createCommandCodeAdapter(provider).buildRequest(parsed()); expect(request).not.toBeInstanceOf(Promise); const built = request as Exclude>; const body = JSON.parse(built.body); - expect(body.config).not.toHaveProperty("isGitRepo"); - expect(body.config).not.toHaveProperty("recentCommits"); + expect(body.config).toHaveProperty("isGitRepo"); + expect(body.config).toHaveProperty("currentBranch"); + expect(body.config).toHaveProperty("mainBranch"); + expect(body.config).toHaveProperty("gitStatus"); + expect(body.config).toHaveProperty("recentCommits"); + expect(Array.isArray(body.config.recentCommits)).toBe(true); expect(body.config.structure).toBeInstanceOf(Array); expect(typeof body.config.workingDir).toBe("string"); expect(built.headers["x-project-slug"]?.length ?? 0).toBeLessThanOrEqual(64); From 56c5ccbb21ee819d05d82f4d40ebdebd3e6362ec Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:21:20 +0200 Subject: [PATCH 06/25] fix(command-code): keep [image] marker in tool-result text in content order The text-only tool-result output now maps image parts to a bounded [image] marker preserving content order, while the follow-up user message still carries the image bytes. Matches the repo convention for text-only wires. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 4 +++- tests/command-code-provider.test.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 50b80a25b..83d7c6c90 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -19,8 +19,10 @@ const COMMAND_CODE_MODEL_ALIASES: Readonly> = { "glm-5.2": "zai-org/GLM-5.2", }; +/** Flatten tool-result content for the text-only wire output, keeping an `[image]` marker per image part in content order. */ function toolResultText(content: string | OcxContentPart[]): string { - return typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join(""); + if (typeof content === "string") return content; + return content.map(part => (part.type === "text" ? part.text : "[image]")).join(""); } function wireMessages(messages: OcxMessage[]): Array> { diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index a601cd89f..9d2807dd5 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -147,7 +147,7 @@ describe("Command Code provider", () => { const built = request as Exclude>; const body = JSON.parse(built.body); expect(body.params.messages).toEqual([ - { role: "tool", content: [{ type: "tool-result", toolCallId: "call_1", toolName: "view_image", output: { type: "text", value: "screenshot:" } }] }, + { role: "tool", content: [{ type: "tool-result", toolCallId: "call_1", toolName: "view_image", output: { type: "text", value: "screenshot:[image]" } }] }, { role: "user", content: [{ type: "image", image }] }, ]); }); From 63c41ba8e1fbc2a3dfbf1b1e01f324892283e266 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:25:29 +0200 Subject: [PATCH 07/25] fix(command-code): preserve live model selection, tool aliases, and paste identity - Skip stale-default healing for live-discovery providers: the OAuth preset has no static models list, so a persisted defaultModel is a user selection and must not be overwritten by the seed on every startup. - Match forced namespaced tool choices by their dot alias via toolChoiceAliases (e.g. functions.exec_command) instead of only the bare or double-underscore wire names. - Carry the whoami-validated identity (userId/userName) through the manual paste fallback so remote/headless logins keep multi-account semantics. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 4 ++-- src/oauth/command-code.ts | 19 ++++++++++++++----- src/oauth/index.ts | 5 ++++- tests/command-code-provider.test.ts | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 83d7c6c90..6bf9b6bfc 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { execFileSync } from "node:child_process"; import { readdirSync } from "node:fs"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; -import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types"; +import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice, toolChoiceAliases } from "../types"; import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; import type { TranslatorBudget } from "../lib/translator-budget"; import { readBoundedResponseBody } from "../lib/bounded-body"; @@ -74,7 +74,7 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] { return tools.filter(tool => toolAllowedByChoice(tool, allowed)); } if (choice && typeof choice !== "string") { - return tools.filter(tool => tool.name === choice.name || namespacedToolName(tool.namespace, tool.name) === choice.name); + return tools.filter(tool => toolChoiceAliases(tool).includes(choice.name)); } return tools; } diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index 263eb67c5..0fafe65d3 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -131,16 +131,23 @@ function createCallbackServer(state: string): { } } -/** Validate a raw pasted Command Code API key against the fixed Provider API host. */ -async function validatePastedApiKey(apiKey: string): Promise { +/** Validate a raw pasted Command Code API key and return the validated identity. */ +async function validatePastedApiKey(apiKey: string): Promise<{ userId: string; userName: string } | undefined> { try { const response = await fetch("https://api.commandcode.ai/alpha/whoami", { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, signal: AbortSignal.timeout(10_000), }); - return response.ok; + if (!response.ok) return undefined; + const body = (await response.json()) as { user?: { id?: unknown; userName?: unknown } }; + const userId = body.user?.id; + const userName = body.user?.userName; + return { + userId: typeof userId === "string" ? userId : "", + userName: typeof userName === "string" ? userName : "", + }; } catch { - return false; + return undefined; } } @@ -194,7 +201,9 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo const input = await ctrl.onManualCodeInput?.(state); if (input === undefined) continue; const pasted = parsePastedCommandCodeInput(input, state); - if (pasted && (await validatePastedApiKey(pasted.apiKey))) return pasted; + if (!pasted) continue; + const identity = await validatePastedApiKey(pasted.apiKey); + if (identity) return { ...pasted, ...identity }; } })() : undefined; diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 6591ec115..0ce73a041 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -783,7 +783,10 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean { changed = true; } // Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot). - if (prov.defaultModel && preset.defaultModel && !(prov.models ?? []).includes(prov.defaultModel)) { + // Skip providers without a static preset `models` list: for live-discovery providers + // (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any + // persisted defaultModel is a user selection and must not be overwritten by the seed. + if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) { prov.defaultModel = preset.defaultModel; changed = true; } diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 9d2807dd5..95c2032f6 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -180,6 +180,21 @@ describe("Command Code provider", () => { expect(JSON.parse((request as Exclude>).body).params.tools).toEqual([]); }); + test("matches a forced namespaced tool choice by dot alias", () => { + const namespacedParsed = { + ...parsed(), + context: { + ...parsed().context, + tools: [{ name: "exec_command", namespace: "functions", description: "exec", parameters: { type: "object" } }], + }, + options: { toolChoice: { name: "functions.exec_command" } }, + }; + const request = createCommandCodeAdapter(provider).buildRequest(namespacedParsed); + expect(request).not.toBeInstanceOf(Promise); + const tools = JSON.parse((request as Exclude>).body).params.tools; + expect(tools).toEqual([{ name: "functions__exec_command", description: "exec", input_schema: { type: "object" } }]); + }); + test("refreshes a stale official effort record only after a reasoning rejection and retries without it", async () => { const requests: Array<{ url: string; body?: string }> = []; const fetch = (async (url: string | URL | Request, init?: RequestInit) => { From dd5594bd1debe05bba3f3065983128de97f1d755 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:30:35 +0200 Subject: [PATCH 08/25] fix(command-code): make workspace/git metadata collection async and bounded - Replace blocking readdirSync/execFileSync with async readdir/execFile so buildRequest never blocks the event loop on a slow or large worktree. - Cache collected metadata per workspace for 30 s so repeated requests reuse it without re-scanning. - Bound each recent-commit entry to 512 chars in addition to the 8-entry cap, keeping the request payload bounded for long commit subjects. - Make buildRequest async (the adapter contract already allows it) and await the config; tests updated accordingly and strengthened to assert the configured bounds. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 56 ++++++++++++++-------- tests/command-code-provider.test.ts | 74 +++++++++++++---------------- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 6bf9b6bfc..f246b40ab 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; -import { execFileSync } from "node:child_process"; -import { readdirSync } from "node:fs"; +import { execFile } from "node:child_process"; +import { readdir } from "node:fs/promises"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice, toolChoiceAliases } from "../types"; import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; @@ -106,8 +106,12 @@ function currentWorkingDirectory(): string | undefined { const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64; /** Cap how many recent commit subjects the config carries. */ const MAX_RECENT_COMMITS = 8; +/** Cap each recent commit entry to keep the request bounded even for long subjects. */ +const MAX_RECENT_COMMIT_LENGTH = 512; /** Cap the git status text sent upstream. */ const MAX_GIT_STATUS_LENGTH = 2048; +/** Keep collected workspace/git metadata fresh for this long (ms) so repeated requests reuse it. */ +const WORKSPACE_METADATA_TTL_MS = 30_000; /** Derive a bounded project slug from the working directory for the `x-project-slug` header. */ function projectSlug(cwd: string): string { @@ -122,36 +126,48 @@ interface GitWorkspaceInfo { recentCommits: string[]; } -/** Best-effort git metadata for the upstream config contract; every read fails safe. */ -function gitWorkspaceInfo(cwd: string | undefined): GitWorkspaceInfo { +const workspaceMetadataCache = new Map(); + +/** Best-effort git metadata for the upstream config contract; every read fails safe and stays off the event loop. */ +async function gitWorkspaceInfo(cwd: string | undefined): Promise { const fallback: GitWorkspaceInfo = { isGitRepo: false, currentBranch: "", mainBranch: "", gitStatus: "", recentCommits: [] }; if (!cwd) return fallback; - const run = (args: string[]): string => { + const cached = workspaceMetadataCache.get(cwd); + if (cached && Date.now() - cached.collectedAt < WORKSPACE_METADATA_TTL_MS) return cached.value; + const run = async (args: string[]): Promise => { try { - return execFileSync("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }).trim(); + const result = await execFile("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true }); + return (result.stdout as unknown as string | null)?.trim() ?? ""; } catch { return ""; } }; - const root = run(["rev-parse", "--show-toplevel"]); - if (!root) return fallback; - return { - isGitRepo: true, - currentBranch: run(["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD", - mainBranch: run(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])?.replace(/^origin\//, "") || run(["rev-parse", "--abbrev-ref", "HEAD"]) || "", - gitStatus: run(["status", "--porcelain"]).slice(0, MAX_GIT_STATUS_LENGTH), - recentCommits: run(["log", "--oneline", `-${MAX_RECENT_COMMITS}`]).split("\n").filter(Boolean).slice(0, MAX_RECENT_COMMITS), - }; + const root = await run(["rev-parse", "--show-toplevel"]); + const value = root + ? { + isGitRepo: true, + currentBranch: (await run(["rev-parse", "--abbrev-ref", "HEAD"])) || "HEAD", + mainBranch: (await run(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "") || (await run(["rev-parse", "--abbrev-ref", "HEAD"])) || "", + gitStatus: (await run(["status", "--porcelain"])).slice(0, MAX_GIT_STATUS_LENGTH), + recentCommits: (await run(["log", "--oneline", `-${MAX_RECENT_COMMITS}`])) + .split("\n") + .filter(Boolean) + .slice(0, MAX_RECENT_COMMITS) + .map(commit => commit.slice(0, MAX_RECENT_COMMIT_LENGTH)), + } + : fallback; + workspaceMetadataCache.set(cwd, { collectedAt: Date.now(), value }); + return value; } -function commandCodeConfig(cwd: string | undefined): Record { +async function commandCodeConfig(cwd: string | undefined): Promise> { let structure: string[] = []; if (cwd) { try { - structure = readdirSync(cwd).filter(name => !name.startsWith(".")).slice(0, MAX_WORKSPACE_STRUCTURE_ENTRIES); + structure = (await readdir(cwd)).filter(name => !name.startsWith(".")).slice(0, MAX_WORKSPACE_STRUCTURE_ENTRIES); } catch { /* workspace metadata is optional */ } } - const git = gitWorkspaceInfo(cwd); + const git = await gitWorkspaceInfo(cwd); return { ...(cwd ? { workingDir: cwd } : {}), date: new Date().toISOString().slice(0, 10), @@ -271,7 +287,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch; return { name: "command-code", - buildRequest(parsed: OcxParsedRequest): AdapterRequest { + async buildRequest(parsed: OcxParsedRequest): Promise { if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code"); const cwd = currentWorkingDirectory(); const tools = visibleTools(parsed); @@ -284,7 +300,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA ].join("\n\n"), parsed.modelId); const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning); const body = { - config: commandCodeConfig(cwd), memory: null, taste: null, skills: null, + config: await commandCodeConfig(cwd), memory: null, taste: null, skills: null, permissionMode: "standard", mode: "agent", params: { model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId, diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 95c2032f6..85382fcda 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -28,6 +28,10 @@ function parsed(modelId = "deepseek/deepseek-v4-flash"): OcxParsedRequest { }; } +async function builtRequest(...args: Parameters["buildRequest"]>) { + return createCommandCodeAdapter(provider).buildRequest(...args); +} + afterEach(() => resetCommandCodeReasoningEffortsForTest()); describe("Command Code provider", () => { @@ -81,7 +85,7 @@ describe("Command Code provider", () => { globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { const href = String(input); calls.push(href); - if (href.includes("whoami")) return new Response(JSON.stringify({ ok: true }), { status: 200 }); + if (href.includes("whoami")) return new Response(JSON.stringify({ ok: true, user: { id: "u-1", userName: "tester" } }), { status: 200 }); throw new Error(`unexpected fetch: ${href}`); }) as typeof globalThis.fetch; try { @@ -91,7 +95,7 @@ describe("Command Code provider", () => { onManualCodeInput: async () => "sk-pasted-key", signal: controller.signal, }, { importLocal: "off" }); - expect(credentials).toMatchObject({ access: "sk-pasted-key", source: "oauth" }); + expect(credentials).toMatchObject({ access: "sk-pasted-key", source: "oauth", accountId: "u-1" }); expect(calls.some(href => href.includes("whoami"))).toBe(true); } finally { globalThis.fetch = originalFetch; @@ -108,10 +112,8 @@ describe("Command Code provider", () => { expect(shouldImportLocalCommandCodeAuth({ importLocal: "off" })).toBe(false); }); - test("builds the proprietary generate request with an officially supported effort and bearer auth", () => { - const request = createCommandCodeAdapter(provider).buildRequest(parsed()); - expect(request).not.toBeInstanceOf(Promise); - const built = request as Exclude>; + test("builds the proprietary generate request with an officially supported effort and bearer auth", async () => { + const built = await builtRequest(parsed()); const body = JSON.parse(built.body); expect(built.url).toBe("https://api.commandcode.ai/alpha/generate"); expect(built.headers.Authorization).toBe("Bearer secret-command-key"); @@ -120,16 +122,14 @@ describe("Command Code provider", () => { expect(built.body).not.toContain("secret-command-key"); }); - test("passes every canonical Command Code id through unchanged", () => { - const request = createCommandCodeAdapter(provider).buildRequest(parsed("xai/grok-4.5")); - expect(request).not.toBeInstanceOf(Promise); - const built = request as Exclude>; + test("passes every canonical Command Code id through unchanged", async () => { + const built = await builtRequest(parsed("xai/grok-4.5")); expect(JSON.parse(built.body).params.model).toBe("xai/grok-4.5"); }); - test("carries tool-result images in a follow-up user message instead of dropping them", () => { + test("carries tool-result images in a follow-up user message instead of dropping them", async () => { const image = "data:image/png;base64,AAAA"; - const request = createCommandCodeAdapter(provider).buildRequest({ + const built = await builtRequest({ ...parsed(), context: { ...parsed().context, @@ -143,8 +143,6 @@ describe("Command Code provider", () => { }], }, }); - expect(request).not.toBeInstanceOf(Promise); - const built = request as Exclude>; const body = JSON.parse(built.body); expect(body.params.messages).toEqual([ { role: "tool", content: [{ type: "tool-result", toolCallId: "call_1", toolName: "view_image", output: { type: "text", value: "screenshot:[image]" } }] }, @@ -152,10 +150,8 @@ describe("Command Code provider", () => { ]); }); - test("keeps the generate config to bounded workspace and git metadata", () => { - const request = createCommandCodeAdapter(provider).buildRequest(parsed()); - expect(request).not.toBeInstanceOf(Promise); - const built = request as Exclude>; + test("keeps the generate config to bounded workspace and git metadata", async () => { + const built = await builtRequest(parsed()); const body = JSON.parse(built.body); expect(body.config).toHaveProperty("isGitRepo"); expect(body.config).toHaveProperty("currentBranch"); @@ -163,24 +159,25 @@ describe("Command Code provider", () => { expect(body.config).toHaveProperty("gitStatus"); expect(body.config).toHaveProperty("recentCommits"); expect(Array.isArray(body.config.recentCommits)).toBe(true); + expect(body.config.recentCommits.length).toBeLessThanOrEqual(8); + expect(body.config.recentCommits.every((entry: string) => entry.length <= 512)).toBe(true); + expect(body.config.gitStatus.length).toBeLessThanOrEqual(2048); expect(body.config.structure).toBeInstanceOf(Array); expect(typeof body.config.workingDir).toBe("string"); expect(built.headers["x-project-slug"]?.length ?? 0).toBeLessThanOrEqual(64); }); - test("does not advertise an unverified effort for models absent from the official table", () => { - const request = createCommandCodeAdapter(provider).buildRequest(parsed("moonshotai/Kimi-K3")); - expect(request).not.toBeInstanceOf(Promise); - expect(JSON.parse((request as Exclude>).body).params).not.toHaveProperty("reasoning_effort"); + test("does not advertise an unverified effort for models absent from the official table", async () => { + const built = await builtRequest(parsed("moonshotai/Kimi-K3")); + expect(JSON.parse(built.body).params).not.toHaveProperty("reasoning_effort"); }); - test("filters tool declarations when tool_choice disables tools", () => { - const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed(), options: { toolChoice: "none" } }); - expect(request).not.toBeInstanceOf(Promise); - expect(JSON.parse((request as Exclude>).body).params.tools).toEqual([]); + test("filters tool declarations when tool_choice disables tools", async () => { + const built = await builtRequest({ ...parsed(), options: { toolChoice: "none" } }); + expect(JSON.parse(built.body).params.tools).toEqual([]); }); - test("matches a forced namespaced tool choice by dot alias", () => { + test("matches a forced namespaced tool choice by dot alias", async () => { const namespacedParsed = { ...parsed(), context: { @@ -189,9 +186,8 @@ describe("Command Code provider", () => { }, options: { toolChoice: { name: "functions.exec_command" } }, }; - const request = createCommandCodeAdapter(provider).buildRequest(namespacedParsed); - expect(request).not.toBeInstanceOf(Promise); - const tools = JSON.parse((request as Exclude>).body).params.tools; + const built = await builtRequest(namespacedParsed); + const tools = JSON.parse(built.body).params.tools; expect(tools).toEqual([{ name: "functions__exec_command", description: "exec", input_schema: { type: "object" } }]); }); @@ -208,19 +204,16 @@ describe("Command Code provider", () => { : new Response("{}", { status: 200 }); }) as typeof globalThis.fetch; const adapter = createCommandCodeAdapter({ ...provider, fetch } as OcxProviderConfig); - const request = adapter.buildRequest({ ...parsed(), options: { reasoning: "max" } }); - expect(request).not.toBeInstanceOf(Promise); - const response = await adapter.fetchResponse!(request as Exclude>); + const request = await adapter.buildRequest({ ...parsed(), options: { reasoning: "max" } }); + const response = await adapter.fetchResponse!(request); expect(response.ok).toBe(true); expect(commandCodeReasoningEfforts("deepseek/deepseek-v4-flash")).toEqual(["high"]); const generated = requests.filter(request => request.url.endsWith("/alpha/generate")); expect(JSON.parse(generated[1]!.body!).params).not.toHaveProperty("reasoning_effort"); }); - test("omits effort when the caller did not choose one", () => { - const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed("claude-haiku-4-5"), options: { maxOutputTokens: 100 } }); - expect(request).not.toBeInstanceOf(Promise); - const built = request as Exclude>; + test("omits effort when the caller did not choose one", async () => { + const built = await builtRequest({ ...parsed("claude-haiku-4-5"), options: { maxOutputTokens: 100 } }); expect(JSON.parse(built.body).params).not.toHaveProperty("reasoning_effort"); }); @@ -263,9 +256,8 @@ describe("Command Code provider", () => { ]); }); - test("sends parsed.stream as the wire stream field", () => { - const request = createCommandCodeAdapter(provider).buildRequest({ ...parsed(), stream: false }); - expect(request).not.toBeInstanceOf(Promise); - expect(JSON.parse((request as Exclude>).body).params.stream).toBe(false); + test("sends parsed.stream as the wire stream field", async () => { + const built = await builtRequest({ ...parsed(), stream: false }); + expect(JSON.parse(built.body).params.stream).toBe(false); }); }); From 701e5232d1dc7b89d061ea9aa9b667fb56004e2f Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:49:36 +0200 Subject: [PATCH 09/25] fix(command-code): use promisified execFile and reject incomplete identities - Wrap execFile with promisify so the git metadata reads actually await captured stdout; the callback-less overload returned a ChildProcess and the workspace always fell back to empty metadata. The metadata test now asserts isGitRepo true and a non-empty branch in the real worktree. - Reject a pasted API key when whoami returns an incomplete identity (missing or empty user.id/userName) instead of storing accountId "". - Yield between manual-paste re-prompts so an abort can interrupt a fast invalid-paste loop; regression test added. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 9 +++++--- src/oauth/command-code.ts | 11 +++++----- tests/command-code-provider.test.ts | 32 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index f246b40ab..8d5c828fa 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; -import { execFile } from "node:child_process"; +import { execFile as execFileCallback } from "node:child_process"; +import { promisify } from "node:util"; import { readdir } from "node:fs/promises"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice, toolChoiceAliases } from "../types"; @@ -128,6 +129,8 @@ interface GitWorkspaceInfo { const workspaceMetadataCache = new Map(); +const execFile = promisify(execFileCallback); + /** Best-effort git metadata for the upstream config contract; every read fails safe and stays off the event loop. */ async function gitWorkspaceInfo(cwd: string | undefined): Promise { const fallback: GitWorkspaceInfo = { isGitRepo: false, currentBranch: "", mainBranch: "", gitStatus: "", recentCommits: [] }; @@ -136,8 +139,8 @@ async function gitWorkspaceInfo(cwd: string | undefined): Promise => { try { - const result = await execFile("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true }); - return (result.stdout as unknown as string | null)?.trim() ?? ""; + const { stdout } = await execFile("git", args, { cwd, encoding: "utf8", timeout: 2000, windowsHide: true }); + return stdout.trim(); } catch { return ""; } diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index 0fafe65d3..473c6b219 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -142,10 +142,9 @@ async function validatePastedApiKey(apiKey: string): Promise<{ userId: string; u const body = (await response.json()) as { user?: { id?: unknown; userName?: unknown } }; const userId = body.user?.id; const userName = body.user?.userName; - return { - userId: typeof userId === "string" ? userId : "", - userName: typeof userName === "string" ? userName : "", - }; + if (typeof userId !== "string" || typeof userName !== "string") return undefined; + if (!userId.trim() || !userName.trim()) return undefined; + return { userId, userName }; } catch { return undefined; } @@ -197,13 +196,15 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo ? (async (): Promise => { while (true) { // The loop keeps waiting until a valid paste arrives; invalid pastes re-prompt. - // `callback`/`timeout` are the only paths that settle the outer race first. + // Yield between iterations so an abort signal can interrupt a fast re-prompt loop. + if (ctrl.signal?.aborted) throw ctrl.signal.reason ?? new DOMException("Command Code login aborted", "AbortError"); const input = await ctrl.onManualCodeInput?.(state); if (input === undefined) continue; const pasted = parsePastedCommandCodeInput(input, state); if (!pasted) continue; const identity = await validatePastedApiKey(pasted.apiKey); if (identity) return { ...pasted, ...identity }; + await new Promise(resolve => setTimeout(resolve, 0)); } })() : undefined; diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 85382fcda..40b32b4e7 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -102,6 +102,34 @@ describe("Command Code provider", () => { } }); + test("rejects a pasted API key whose whoami identity is incomplete", async () => { + const controller = new AbortController(); + const originalFetch = globalThis.fetch; + let whoamiCalls = 0; + globalThis.fetch = (async (input: string | URL | Request) => { + const href = String(input); + if (href.includes("whoami")) { + whoamiCalls += 1; + return new Response(JSON.stringify({ ok: true, user: { id: "", userName: "" } }), { status: 200 }); + } + throw new Error(`unexpected fetch: ${href}`); + }) as typeof globalThis.fetch; + try { + // With an incomplete identity, the manual loop keeps re-prompting; abort to stop it. + const login = loginCommandCode({ + onAuth: () => {}, + onProgress: () => {}, + onManualCodeInput: async () => "sk-incomplete-key", + signal: controller.signal, + }, { importLocal: "off" }); + controller.abort(new Error("cancelled")); + await expect(login).rejects.toThrow("cancelled"); + expect(whoamiCalls).toBeGreaterThan(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("uses live account discovery and only imports local CLI auth for the first account", () => { const request = buildModelsRequest(provider, "secret-command-key", "command-code"); expect(request).toEqual({ @@ -165,6 +193,10 @@ describe("Command Code provider", () => { expect(body.config.structure).toBeInstanceOf(Array); expect(typeof body.config.workingDir).toBe("string"); expect(built.headers["x-project-slug"]?.length ?? 0).toBeLessThanOrEqual(64); + // This test runs inside a git worktree, so the real (non-fallback) metadata path + // must populate the repo/branch instead of returning the empty fallback. + expect(body.config.isGitRepo).toBe(true); + expect(body.config.currentBranch).not.toBe(""); }); test("does not advertise an unverified effort for models absent from the official table", async () => { From 5df6ef3d16211edd38459377dabab13358bd1928 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:53:05 +0200 Subject: [PATCH 10/25] fix(command-code): stream upstream always, resolve instruction aliases, keep identity - Always request stream:true from /alpha/generate (NDJSON-only endpoint) and let the proxy convert to the client's requested shape, so non-stream clients do not hit an unparseable JSON body or upstream rejection. - Resolve forced tool-choice names to the advertised wire name in the tool instruction too, so the model is told to call a tool that is in the catalog. - Preserve the whoami-validated identity when importing local CLI auth, so an imported credential keeps multi-account semantics even when auth.json omits userId. - Reject pasted callback URLs/query strings whose state does not match the current login, mirroring the shared OAuth callback flow; raw in-session keys stay exempt. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 11 ++++++++--- src/oauth/command-code.ts | 12 +++++++++++- tests/command-code-provider.test.ts | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 8d5c828fa..5f68eae34 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -3,7 +3,7 @@ import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { readdir } from "node:fs/promises"; import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types"; -import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice, toolChoiceAliases } from "../types"; +import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice, toolChoiceAliases } from "../types"; import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base"; import type { TranslatorBudget } from "../lib/translator-budget"; import { readBoundedResponseBody } from "../lib/bounded-body"; @@ -86,7 +86,10 @@ function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined { return "Tool choice is required for this turn. Make at least one call from the advertised tool catalog before answering."; } if (choice && typeof choice !== "string" && !isAllowedToolChoice(choice)) { - return `Tool choice is required for this turn. Call the advertised tool named ${namespacedToolName(undefined, choice.name)} before answering.`; + // Resolve the forced name (bare, namespace__name, or namespace.name) to the advertised + // wire name so the instruction names a tool that is actually in the catalog. + const wireName = resolveToolChoiceWireName(parsed.context.tools, choice.name); + return `Tool choice is required for this turn. Call the advertised tool named ${wireName} before answering.`; } return undefined; } @@ -311,7 +314,9 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA tools: wireTools(tools), system, max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000, - stream: parsed.stream, + // The proprietary /alpha/generate endpoint is NDJSON-stream-only; the proxy converts + // the buffered/streamed events to the client's requested shape (parsed.stream). + stream: true, ...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}), ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), }, diff --git a/src/oauth/command-code.ts b/src/oauth/command-code.ts index 473c6b219..4f3217702 100644 --- a/src/oauth/command-code.ts +++ b/src/oauth/command-code.ts @@ -42,21 +42,27 @@ async function importLocalCommandCodeAuth(signal?: AbortSignal): Promise 0) accountId = body.user.id; } catch (error) { if (signal?.aborted) throw signal.reason ?? new DOMException("Command Code login aborted", "AbortError"); return undefined; } + if (!accountId && typeof parsed.userId === "string" && parsed.userId.length > 0) accountId = parsed.userId; return { access: parsed.apiKey, refresh: parsed.apiKey, expires: Number.MAX_SAFE_INTEGER, - ...(typeof parsed.userId === "string" && parsed.userId.length > 0 ? { accountId: parsed.userId } : {}), + ...(accountId ? { accountId } : {}), source: "local-cli", }; } @@ -164,6 +170,10 @@ function parsePastedCommandCodeInput(input: string, expectedState: string): Comm const parsed = parseCallbackInput(trimmed); const apiKey = parsed.code?.trim(); if (!apiKey) return undefined; + // A URL/query-shaped paste is an authorization response and must carry a matching state, + // mirroring the shared OAuth callback flow; a stale or attacker-supplied URL from another + // session must not be accepted. Raw in-session keys are exempt (no state to compare). + if (parsed.kind !== "raw" && parsed.state !== expectedState) return undefined; return { apiKey, state: expectedState, userId: "", userName: "", keyName: "manual" }; } diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 40b32b4e7..3d86cddcb 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -288,8 +288,8 @@ describe("Command Code provider", () => { ]); }); - test("sends parsed.stream as the wire stream field", async () => { + test("always requests streaming upstream so non-stream clients still get NDJSON", async () => { const built = await builtRequest({ ...parsed(), stream: false }); - expect(JSON.parse(built.body).params.stream).toBe(false); + expect(JSON.parse(built.body).params.stream).toBe(true); }); }); From 1e2095de893a87036b41f4c1808331cb02b4a86e Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:02:57 +0200 Subject: [PATCH 11/25] feat(command-code): distinguish OAuth and API presets in the UI - Rename the OAuth account preset to "Command Code - Auth" and the key/API preset to "Command Code - API" so the two catalog entries are no longer indistinguishable in the dashboard. - Split the single provider.name.commandCode i18n key into provider.name.commandCodeAuth / provider.name.commandCodeApi across all six locales and point each provider id at its own key. - Update registry labels, key-login derivation, and the display-name tests. Co-authored-by: CommandCodeBot --- gui/src/i18n/de.ts | 3 ++- gui/src/i18n/en.ts | 3 ++- gui/src/i18n/ja.ts | 3 ++- gui/src/i18n/ko.ts | 3 ++- gui/src/i18n/ru.ts | 3 ++- gui/src/i18n/zh.ts | 3 ++- gui/src/provider-icons.ts | 4 ++-- src/providers/registry.ts | 4 ++-- tests/commandcode-provider.test.ts | 4 ++-- tests/provider-workspace-data.test.ts | 6 +++--- 10 files changed, 21 insertions(+), 15 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 97c204ba2..43d29a0f5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -37,7 +37,8 @@ export const de: Record = { "theme.dark": "Dunkel", "theme.system": "System", "lang.label": "Sprache", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark Coding-Tarif", "provider.name.volcengineAgentPlan": "Volcengine Ark Agent-Tarif", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index a6f1e054e..0583bd7d0 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -44,7 +44,8 @@ export const en = { "theme.dark": "Dark", "theme.system": "System", "lang.label": "Language", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan", "provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 499d7e735..df6d3ef79 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -42,7 +42,8 @@ export const ja: Record = { "theme.dark": "ダーク", "theme.system": "システム", "lang.label": "言語", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark コーディングプラン", "provider.name.volcengineAgentPlan": "Volcengine Ark エージェントプラン", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index c94838288..4529fedd3 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -37,7 +37,8 @@ export const ko: Record = { "theme.dark": "다크", "theme.system": "시스템", "lang.label": "언어", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark 코딩 플랜", "provider.name.volcengineAgentPlan": "Volcengine Ark 에이전트 플랜", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 4194febad..0a5ad7444 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -42,7 +42,8 @@ export const ru: Record = { "theme.dark": "Тёмная", "theme.system": "Системная", "lang.label": "Язык", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark — тариф Coding", "provider.name.volcengineAgentPlan": "Volcengine Ark — тариф Agent", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7b471dbfd..6c08b7110 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -37,7 +37,8 @@ export const zh: Record = { "theme.dark": "深色", "theme.system": "跟随系统", "lang.label": "语言", - "provider.name.commandCode": "Command Code", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", "provider.name.volcengine": "火山方舟", "provider.name.volcengineCodingPlan": "火山方舟编程套餐", "provider.name.volcengineAgentPlan": "火山方舟智能体套餐", diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 60a8da30e..4516766b2 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -99,8 +99,8 @@ const PROVIDER_DISPLAY_NAMES: Record = { }; const PROVIDER_DISPLAY_NAME_KEYS: Record = { - "command-code": "provider.name.commandCode", - commandcode: "provider.name.commandCode", + "command-code": "provider.name.commandCodeAuth", + commandcode: "provider.name.commandCodeApi", volcengine: "provider.name.volcengine", "volcengine-coding-plan": "provider.name.volcengineCodingPlan", "volcengine-agent-plan": "provider.name.volcengineAgentPlan", diff --git a/src/providers/registry.ts b/src/providers/registry.ts index ffd135074..f4926b3c0 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -802,7 +802,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, { id: "command-code", - label: "Command Code", + label: "Command Code - Auth", adapter: "command-code", baseUrl: "https://api.commandcode.ai", authKind: "oauth", @@ -1283,7 +1283,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ }, { id: "commandcode", - label: "Command Code", + label: "Command Code - API", adapter: "openai-chat", baseUrl: "https://api.commandcode.ai/provider/v1", authKind: "key", diff --git a/tests/commandcode-provider.test.ts b/tests/commandcode-provider.test.ts index db037e378..c1daadbc7 100644 --- a/tests/commandcode-provider.test.ts +++ b/tests/commandcode-provider.test.ts @@ -58,7 +58,7 @@ describe("Command Code provider", () => { const entry = commandcodeEntry(); expect(entry).toMatchObject({ id: "commandcode", - label: "Command Code", + label: "Command Code - API", adapter: "openai-chat", baseUrl: "https://api.commandcode.ai/provider/v1", authKind: "key", @@ -82,7 +82,7 @@ describe("Command Code provider", () => { test("derives key-login, init, and dashboard presets without persisting trust policy", () => { expect(KEY_LOGIN_PROVIDERS.commandcode).toMatchObject({ - label: "Command Code", + label: "Command Code - API", adapter: "openai-chat", baseUrl: "https://api.commandcode.ai/provider/v1", dashboardUrl: "https://commandcode.ai/studio/", diff --git a/tests/provider-workspace-data.test.ts b/tests/provider-workspace-data.test.ts index 5e3f98184..8cc93b7ae 100644 --- a/tests/provider-workspace-data.test.ts +++ b/tests/provider-workspace-data.test.ts @@ -447,9 +447,9 @@ describe("provider-icons", () => { expect(formatProviderDisplayName("chatgpt", englishT)).toBe("ChatGPT"); }); - test("Command Code account and API-key presets share the catalog display name", () => { - expect(formatProviderDisplayName("command-code", englishT)).toBe("Command Code"); - expect(formatProviderDisplayName("commandcode", englishT)).toBe("Command Code"); + test("Command Code account and API-key presets use distinct display names", () => { + expect(formatProviderDisplayName("command-code", englishT)).toBe("Command Code - Auth"); + expect(formatProviderDisplayName("commandcode", englishT)).toBe("Command Code - API"); expect(isCatalogProviderId("command-code")).toBe(true); expect(isCatalogProviderId("commandcode")).toBe(true); }); From db7cc87a58642f643c5705d1c83d915379e70e57 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:10:08 +0200 Subject: [PATCH 12/25] feat(command-code): use the Command Code icon and drop duplicate-id subtext - Add the commandcode-color.svg icon (from dev) to the PR tree and point both the command-code and commandcode provider ids at it, so the dashboard shows the real Command Code brand mark instead of the generic fallback letter. - With distinct "Auth"/"API" display names the workspace rail no longer treats the two entries as duplicates, so the config-id subtext (command-code / commandcode) disappears and only the model count remains. - Assert icon resolution in the display-name test. Co-authored-by: CommandCodeBot --- gui/public/provider-icons/commandcode-color.svg | 1 + gui/src/provider-icons.ts | 2 ++ tests/provider-workspace-data.test.ts | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 gui/public/provider-icons/commandcode-color.svg diff --git a/gui/public/provider-icons/commandcode-color.svg b/gui/public/provider-icons/commandcode-color.svg new file mode 100644 index 000000000..4f257b4ec --- /dev/null +++ b/gui/public/provider-icons/commandcode-color.svg @@ -0,0 +1 @@ +Command Code diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts index 4516766b2..666f2cdd0 100644 --- a/gui/src/provider-icons.ts +++ b/gui/src/provider-icons.ts @@ -9,6 +9,8 @@ const PROVIDER_ICON_ALIASES: Record = { "cloudflare-workers-ai": "cloudflare-ai-gateway-color.svg", cline: "cline-color.svg", "cline-pass": "cline-color.svg", + "command-code": "commandcode-color.svg", + commandcode: "commandcode-color.svg", cursor: "cursor-color.svg", deepseek: "deepseek-color.svg", firepass: "firepass-color.svg", diff --git a/tests/provider-workspace-data.test.ts b/tests/provider-workspace-data.test.ts index 8cc93b7ae..392519977 100644 --- a/tests/provider-workspace-data.test.ts +++ b/tests/provider-workspace-data.test.ts @@ -25,6 +25,7 @@ import { import { formatProviderDisplayName, isCatalogProviderId, + providerIconSrc, } from "../gui/src/provider-icons"; import { en } from "../gui/src/i18n/en"; import { interpolate, type TFn } from "../gui/src/i18n/shared"; @@ -452,6 +453,8 @@ describe("provider-icons", () => { expect(formatProviderDisplayName("commandcode", englishT)).toBe("Command Code - API"); expect(isCatalogProviderId("command-code")).toBe(true); expect(isCatalogProviderId("commandcode")).toBe(true); + expect(providerIconSrc("command-code")).toBe("/provider-icons/commandcode-color.svg"); + expect(providerIconSrc("commandcode")).toBe("/provider-icons/commandcode-color.svg"); }); test("unknown simple ids are title-cased; mixedCase custom names pass through", () => { From 3abf56419c8ec1af8a42c8bf4cae4f744377e407 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:14:39 +0200 Subject: [PATCH 13/25] fix(gui): use official OpenAI green brand mark The PR branch predates dev commit b1439846 which corrected openai.svg from the wrong purple (#412991) to the official OpenAI green (#10A37F). Pull that correction into the PR so the OpenAI/Codex entry shows the brand-green mark. Co-authored-by: CommandCodeBot --- gui/public/provider-icons/openai.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/public/provider-icons/openai.svg b/gui/public/provider-icons/openai.svg index a25156f3b..ef1ef3096 100644 --- a/gui/public/provider-icons/openai.svg +++ b/gui/public/provider-icons/openai.svg @@ -1 +1 @@ -OpenAI \ No newline at end of file +OpenAI From f01f39b1d9c48027ce9f22d05f0842f20220b049 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:23:35 +0200 Subject: [PATCH 14/25] fix(command-code): align adapter with the /alpha/generate wire protocol - Send memory as the schema-strict empty string instead of null. - Treat finish-step as a terminal event (usage + finishReason) and emit only one done even when finish-step and finish both appear in the stream. - Defensively strip SSE data: framing so a gateway shape drift cannot silently drop every event; raw newline-delimited JSON still parses as before. - Make x-command-code-version configurable via provider.commandCodeVersion and update the default to the current CLI protocol version. - Add finish-step and SSE-frame regression tests. Co-authored-by: CommandCodeBot --- src/adapters/command-code.ts | 25 +++++++++++++++++++------ src/types.ts | 6 ++++++ tests/command-code-provider.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 5f68eae34..049aa633b 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -228,7 +228,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera let newline = buffer.indexOf("\n"); while (newline >= 0) { const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); - if (line) { try { yield JSON.parse(line) as Record; } catch { /* ignore non-events */ } } + if (line) { try { yield JSON.parse(stripEventFrame(line)) as Record; } catch { /* ignore non-events */ } } newline = buffer.indexOf("\n"); } const residualBytes = encoder.encode(buffer).byteLength; @@ -239,7 +239,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera if (done) break; } const final = buffer.trim(); - if (final) { try { yield JSON.parse(final) as Record; } catch { /* ignore */ } } + if (final) { try { yield JSON.parse(stripEventFrame(final)) as Record; } catch { /* ignore */ } } } finally { budget.releaseRetained(bufferBytes, { kind: "live_transient" }); try { await reader.cancel(); } catch { /* already closed */ } @@ -247,6 +247,11 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera } } +/** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */ +function stripEventFrame(line: string): string { + return line.startsWith("data:") ? line.slice("data:".length).trim() : line; +} + function isReasoningEffortRejection(status: number, payload: string): boolean { return (status === 400 || status === 422) && /reasoning[_ -]?effort|unsupported effort|invalid effort/i.test(payload); } @@ -306,7 +311,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA ].join("\n\n"), parsed.modelId); const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning); const body = { - config: await commandCodeConfig(cwd), memory: null, taste: null, skills: null, + config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null, permissionMode: "standard", mode: "agent", params: { model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId, @@ -325,7 +330,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA Authorization: `Bearer ${provider.apiKey}`, "Content-Type": "application/json", "User-Agent": "cli", - "x-command-code-version": "1.12.0", + "x-command-code-version": provider.commandCodeVersion ?? "0.52.1", "x-cli-environment": "production", "x-taste-learning": "false", "x-co-flag": "false", @@ -387,10 +392,18 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA } break; } - case "finish": + case "finish-step": + case "finish": { + // Both events are terminal on the current wire; streams commonly carry both, so + // only the first one emits the done. finish-step carries `usage`; finish may carry + // `totalUsage` (current flows) or `usage`. + if (sawFinish) break; sawFinish = true; - yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; + const usageValue = event.totalUsage ?? event.usage; + const stopReason = typeof event.rawFinishReason === "string" ? event.rawFinishReason : typeof event.finishReason === "string" ? event.finishReason : undefined; + yield { type: "done", usage: usage(usageValue), stopReason }; break; + } case "error": yield { type: "error", message: eventError(event.error), status: 502 }; break; } } diff --git a/src/types.ts b/src/types.ts index 18c8a6339..171628caf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1066,6 +1066,12 @@ export interface OcxProviderConfig { * the legacy `/v1/responses` construction. */ responsesPath?: string; + /** + * Command Code protocol version sent as `x-command-code-version` on /alpha/generate requests. + * The internal endpoint's schema drifts with the CLI version; operators can pin a known-good + * version here instead of waiting for a code change. Absent uses the adapter's current default. + */ + commandCodeVersion?: string; /** * Responses upstream that stores nothing server-side (DeepSeek documents "the API * is stateless"). Stateful request parameters are dropped, `store` is pinned false, diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index 3d86cddcb..090a3d0fd 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -288,6 +288,35 @@ describe("Command Code provider", () => { ]); }); + test("strips SSE data: framing if the gateway ever switches stream shapes", async () => { + const response = new Response([ + "data: " + JSON.stringify({ type: "text-delta", text: "a" }), + "data: " + JSON.stringify({ type: "text-delta", text: "b" }), + "data: [DONE]", + ].join("\n")); + const events = []; + for await (const event of createCommandCodeAdapter(provider).parseStream(response, createTestTranslatorBudget())) events.push(event); + expect(events).toEqual([ + { type: "text_delta", text: "a" }, + { type: "text_delta", text: "b" }, + { type: "done", usage: undefined, stopReason: undefined }, + ]); + }); + + test("treats finish-step as a terminal event and emits only one done", async () => { + const response = new Response([ + JSON.stringify({ type: "text-delta", text: "hi" }), + JSON.stringify({ type: "finish-step", finishReason: "stop", usage: { inputTokens: 10, outputTokens: 4, totalTokens: 14 } }), + JSON.stringify({ type: "finish", rawFinishReason: "stop", totalUsage: { inputTokens: 10, outputTokens: 4, totalTokens: 14 } }), + ].join("\n")); + const events = []; + for await (const event of createCommandCodeAdapter(provider).parseStream(response, createTestTranslatorBudget())) events.push(event); + expect(events).toEqual([ + { type: "text_delta", text: "hi" }, + { type: "done", usage: { inputTokens: 10, outputTokens: 4, totalTokens: 14 }, stopReason: "stop" }, + ]); + }); + test("always requests streaming upstream so non-stream clients still get NDJSON", async () => { const built = await builtRequest({ ...parsed(), stream: false }); expect(JSON.parse(built.body).params.stream).toBe(true); From aff4dd6f199d5dda54d2f886a8220b8dc5cfc92f Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:37:11 +0200 Subject: [PATCH 15/25] fix(gui): show distinct provider display names across all surfaces The Command Code OAuth and API presets were only distinct in the provider workspace; data-driven surfaces rendered the raw config id (command-code / commandcode), so both still read as "Command Code". Route the provider id through formatProviderDisplayName in: - Models tab group headers and model tooltip - Logs provider column, detail row, and attempt rows - Usage per-model and per-provider tables - API keys source label - Dashboard active-providers table and dashboard models groups - Combo target provider select Route/path strings (e.g. command-code/model selects and cost breakdowns) keep the raw id since it is the routing key and already disambiguates. Co-authored-by: CommandCodeBot --- gui/src/components/combo-workspace-controls.tsx | 3 ++- gui/src/pages/ApiKeys.tsx | 3 ++- gui/src/pages/Logs.tsx | 7 ++++--- gui/src/pages/Models.tsx | 5 +++-- gui/src/pages/Usage.tsx | 5 +++-- gui/src/pages/dashboard-models-section.tsx | 3 ++- gui/src/pages/dashboard-providers-section.tsx | 3 ++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 5126df734..8a7277fd4 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -3,6 +3,7 @@ import type { ComboEffort, ComboStrategy, ComboTarget } from "../combo-workspace import { COMBO_EFFORTS, newComboTarget } from "../combo-workspace-data"; import { IconArrowDown, IconArrowUp, IconGrip, IconPlus, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; +import { formatProviderDisplayName } from "../provider-icons"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; import { clampedNumberInput, enabledProviders, modelsForProvider } from "./combo-workspace-utils"; @@ -199,7 +200,7 @@ export function TargetEditor({ {providerOptions.map((p) => ( ))} diff --git a/gui/src/pages/ApiKeys.tsx b/gui/src/pages/ApiKeys.tsx index b920fc375..7b2358412 100644 --- a/gui/src/pages/ApiKeys.tsx +++ b/gui/src/pages/ApiKeys.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { Notice } from "../ui"; import { useI18n, LOCALES } from "../i18n/shared"; +import { formatProviderDisplayName } from "../provider-icons"; import { readJsonIfOk, readJsonOrThrow } from "../fetch-json"; import { classifyExternalModel, @@ -320,7 +321,7 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a if (model.native) return t("api.sourceNative"); if (model.provider === "combo") return t("api.sourceCombo"); if (model.custom) return t("api.sourceCustom"); - return model.provider; + return formatProviderDisplayName(model.provider, t); }; const protocolLabel = (protocol: GatewayInboundProtocol): string => { diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index ebe177532..e67aeb389 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { useI18n, LOCALES, type TFn } from "../i18n/shared"; +import { formatProviderDisplayName } from "../provider-icons"; import { formatTokens } from "../format-tokens"; import { hashLogConversationQuery, matchesLogConversationId } from "../log-conversation-id"; import { statusCodeInfo } from "../status-codes"; @@ -774,7 +775,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { {reasoningWire && {reasoningWire}} - {log.provider} + {formatProviderDisplayName(log.provider, t)} {log.status} @@ -912,7 +913,7 @@ function LogDetailDialog({ )} {t("logs.col.model")}{modelLabel(detail.resolvedModel ?? detail.model)} - {t("logs.col.provider")}{detail.provider} + {t("logs.col.provider")}{formatProviderDisplayName(detail.provider, t)} {(detail.requestedEffort || detail.effectiveEffort) && ( <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} )} @@ -997,7 +998,7 @@ function LogDetailDialog({ {attempt.ordinal} - {attempt.provider}
+ {formatProviderDisplayName(attempt.provider, t)}
{attempt.model} {(attempt.requestedEffort || attempt.effectiveEffort) && ( <> diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index f2ff909c3..5dffee9b8 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -4,6 +4,7 @@ import { IconChevron, IconBoxes, IconInfo, IconShuffle, IconCheck, IconAlert } f import { useT } from "../i18n/shared"; import type { TFn, TKey } from "../i18n/shared"; import { modelLabel } from "../model-display"; +import { formatProviderDisplayName } from "../provider-icons"; import { type ComboItem, parseComboList } from "../combo-workspace-data"; import { readJsonIfOk, readJsonOrThrow } from "../fetch-json"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; @@ -723,7 +724,7 @@ export default function Models({ apiBase }: { apiBase: string }) { style={{ flex: 1, border: 0, background: "transparent", padding: 0, color: "inherit", cursor: "pointer", textAlign: "left" }} > - {provider} + {formatProviderDisplayName(provider, t)} {isNative && {t("models.nativeGroupLabel")}} {discoveryFailure && ( {t("models.tipProvider")} - {m.provider} + {formatProviderDisplayName(m.provider, t)} {(m.contextWindow || m.contextCap) && ( <> {t("models.tipContext")} diff --git a/gui/src/pages/Usage.tsx b/gui/src/pages/Usage.tsx index acd0b78de..afca8218f 100644 --- a/gui/src/pages/Usage.tsx +++ b/gui/src/pages/Usage.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useI18n, type TFn, type Locale } from "../i18n/shared"; +import { formatProviderDisplayName } from "../provider-icons"; import { formatTokens } from "../format-tokens"; import { formatEstimatedUsdValue as formatUsdEstimate } from "../intl-formatters"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; @@ -512,7 +513,7 @@ function UsageModelsTable({ {models.map(model => ( {modelLabel(model.model)} - {model.provider} + {formatProviderDisplayName(model.provider, t)} {model.requests} {model.measuredRequests} {formatTokens(model.totalTokens, locale)} @@ -572,7 +573,7 @@ function UsageProvidersTable({ {providers.map(provider => ( - {provider.provider} + {formatProviderDisplayName(provider.provider, t)} {provider.requests} {provider.measuredRequests} {formatTokens(provider.totalTokens, locale)} diff --git a/gui/src/pages/dashboard-models-section.tsx b/gui/src/pages/dashboard-models-section.tsx index a3d2f1af2..b839828e5 100644 --- a/gui/src/pages/dashboard-models-section.tsx +++ b/gui/src/pages/dashboard-models-section.tsx @@ -2,6 +2,7 @@ import { type Dispatch, type SetStateAction } from "react"; import { IconChevron, IconSearch } from "../icons"; import type { TFn } from "../i18n/shared"; import { EmptyState } from "../ui"; +import { formatProviderDisplayName } from "../provider-icons"; import type { ModelInfo } from "./dashboard-shared"; export function DashboardModelsSection({ @@ -60,7 +61,7 @@ export function DashboardModelsSection({ aria-expanded={open} >