From 690f5739ad3a4bc9d1a3ce9f28ab98ed8f9b8bdc Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Fri, 21 Aug 2026 20:28:45 -0300 Subject: [PATCH] fix(command-code): use documented /provider/v1 chat endpoint (#10265) --- .../fixes/10265-command-code-provider-api.md | 1 + .../providers/registry/command-code/index.ts | 6 +- open-sse/executors/commandCode.ts | 977 +----------------- src/lib/providers/validation/openaiFormat.ts | 38 +- .../constants/providers/apikey/gateways.ts | 2 +- tests/unit/command-code-executor.test.ts | 662 +++--------- ...mmand-code-maxtokens-negative-5166.test.ts | 24 +- .../unit/command-code-user-array-5166.test.ts | 241 ++--- tests/unit/command-code-vision.test.ts | 657 ++---------- tests/unit/executor-command-code.test.ts | 347 ++----- ...vider-models-target-format-scoping.test.ts | 4 +- .../provider-validation-specialty.test.ts | 34 +- tests/unit/responses-handler.test.ts | 30 +- 13 files changed, 527 insertions(+), 2496 deletions(-) create mode 100644 changelog.d/fixes/10265-command-code-provider-api.md diff --git a/changelog.d/fixes/10265-command-code-provider-api.md b/changelog.d/fixes/10265-command-code-provider-api.md new file mode 100644 index 0000000000..b38e4e9d2a --- /dev/null +++ b/changelog.d/fixes/10265-command-code-provider-api.md @@ -0,0 +1 @@ +- fix(command-code): route chat to the documented /provider/v1/chat/completions endpoint instead of the CLI-only /alpha/generate, which Command Code gates/blocks for external callers (#10265) \ No newline at end of file diff --git a/open-sse/config/providers/registry/command-code/index.ts b/open-sse/config/providers/registry/command-code/index.ts index 77b08ab8ce..f298bcc21f 100644 --- a/open-sse/config/providers/registry/command-code/index.ts +++ b/open-sse/config/providers/registry/command-code/index.ts @@ -8,7 +8,11 @@ export const command_codeProvider: RegistryEntry = { format: "openai", executor: "command-code", baseUrl: "https://api.commandcode.ai", - chatPath: "/alpha/generate", + // Chat uses the documented /provider/v1/chat/completions (OpenAI-format) + // endpoint — NOT the CLI-only /alpha/generate endpoint, which Command Code + // version-gates and proxy-blocks for external callers (#10265). Discovery + // already targets the sibling /provider/v1/models endpoint. + chatPath: "/provider/v1/chat/completions", modelsUrl: "https://api.commandcode.ai/provider/v1/models", // The discovery response is a partial routing catalog; static registry // entries omitted from it can still be accepted by the gateway. diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts index b736e6cdf1..a1023fc80c 100644 --- a/open-sse/executors/commandCode.ts +++ b/open-sse/executors/commandCode.ts @@ -1,6 +1,3 @@ -import { randomUUID } from "node:crypto"; - -import { isVisionModelId } from "@/shared/constants/visionModels"; import { REGISTRY } from "../config/providerRegistry.ts"; import { BaseExecutor, @@ -11,407 +8,54 @@ import { type JsonRecord = Record; -export const COMMAND_CODE_VERSION = process.env.COMMAND_CODE_VERSION?.trim() || "1.15.1"; -// Hard server-side ceiling enforced by Command Code's /alpha/generate endpoint: -// any request with params.max_tokens > 200_000 is rejected with a 400 -// "Too big: expected number to be <=200000 at params.max_tokens". We only use -// this to clamp a CLIENT-SUPPLIED max_tokens down to a value the endpoint will -// accept; we never fabricate this number for requests that omit the field (see -// clampMaxTokens / buildCommandCodeBody). +// Defensive server-side ceiling for a CLIENT-SUPPLIED max_tokens. The official +// /provider/v1/chat/completions endpoint (documented OpenAI-format surface) is +// the successor to the CLI-only /alpha/generate endpoint, which rejected any +// params.max_tokens > 200_000 with a 400. We only clamp a client-supplied value +// down; we never fabricate this number for requests that omit the field (see +// clampMaxTokens / buildOpenAiBody). const MAX_COMMAND_CODE_TOKENS = 200_000; -const encoder = new TextEncoder(); function isRecord(value: unknown): value is JsonRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } -function asRecordArray(value: unknown): JsonRecord[] { - return Array.isArray(value) ? value.filter(isRecord) : []; -} - -function stringValue(value: unknown): string | undefined { - return typeof value === "string" ? value : undefined; -} - function numberValue(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } -function recordOrEmpty(value: unknown): JsonRecord { - if (isRecord(value)) return value; - if (typeof value === "string" && value.trim()) { - try { - const parsed: unknown = JSON.parse(value); - if (isRecord(parsed)) return parsed; - } catch (error) { - console.warn( - "[commandCode] tool arg parse failed:", - error instanceof Error ? error.message : String(error) - ); - } - } - return {}; -} - -/** - * Build the `arguments` field for an assistant tool-call part that Command - * Code's /alpha/generate schema REQUIRES (rejects a missing field with - * `missing required field 'arguments'`). Valid source values round-trip: - * - object arguments -> JSON string of the object - * - valid JSON string arguments -> the string as-is - * - missing / empty / invalid JSON string -> "{}" (a valid empty-object string) - */ -function toolCallArgumentsString(value: unknown): string { - if (isRecord(value)) return JSON.stringify(value); - if (typeof value === "string" && value.trim()) { - try { - const parsed: unknown = JSON.parse(value); - if (isRecord(parsed)) return value; - } catch { - return "{}"; - } - return "{}"; - } - return JSON.stringify(recordOrEmpty(value)); -} - -/** - * Tool names that collide with Command Code's server-side built-in tools. - * The /alpha/generate server normalizes tool-call/tool-result parts against - * ITS OWN built-in registry for matching names; for its built-in `tool_search` - * the result normalization requires `arguments` in a shape we do not send, so - * the result is rejected with `input[N] missing required field 'arguments'` - * (verified live 2026-08-10 — renaming the call/result `tool_search` → `grep` - * makes the identical request pass; the server pairs each tool-result with the - * nearest preceding tool-call, so any result following such a call is affected). - * We rename the colliding name consistently on the wire — definitions, calls - * and results — then un-rename on the response path so the client still sees - * its original tool names. - */ -const COMMAND_CODE_RESERVED_TOOL_NAMES = new Set(["tool_search"]); - -function wireToolName(clientName: string, toolNameMap: Map): string { - if (COMMAND_CODE_RESERVED_TOOL_NAMES.has(clientName)) { - const wire = `omniroute_${clientName}`; - toolNameMap.set(wire, clientName); - return wire; - } - return clientName; -} - -function clientToolName(wireName: string, toolNameMap: Map): string { - return toolNameMap.get(wireName) ?? wireName; -} - -function normalizeContentText(content: unknown): string { - if (typeof content === "string") return content; - return asRecordArray(content) - .filter((part) => part.type === "text") - .map((part) => stringValue(part.text) || "") - .join("\n"); -} - -/** - * Model id patterns for Command Code models that have `text, vision` - * capability per the official CC model registry, but are NOT caught - * by the shared {@link isVisionModelId} heuristic. Kept as a local - * set because these are CC-specific model IDs (vendor-prefix shapes - * like "moonshotai/Kimi-K2.6" or CC aliases like "gpt-5.6-luna"). - * - * Source: Command Code /alpha/generate model registry (docs). - */ -const CC_VISION_MODEL_PATTERNS: readonly RegExp[] = [ - // Open Source - /kimi-k2/i, // moonshotai/Kimi-K2.6, Kimi-K2.7-Code, Kimi-K2.5 - /qwen3\.\d/i, // Qwen/Qwen3.6-Plus, Qwen/Qwen3.7-Plus - /step-?3/i, // stepfun/Step-3.7-Flash - // Anthropic - /claude-fable/i, // claude-fable-5 (not covered by claude-opus/sonnet/haiku-4) - // OpenAI - /gpt-5/i, // gpt-5.6, gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex - // NOTE: gpt-5.4-mini and gpt-5.3-codex deliberately stay inside the `/gpt-5/` - // family — both accept image input on the OpenAI API, and there is no - // verified Command Code backend data marking them text-only. Excluding them - // without evidence would re-create #4071 (image stripped from a model that - // can see it). Revisit only with per-model CC registry capability data. - // Sakana - /fugu/i, // sakana/fugu-ultra -]; - -/** - * Whether a model id routed through the Command Code executor is - * vision-capable. Checks Mimo-specific rules first, then CC-specific - * patterns, then falls through to the shared {@link isVisionModelId} - * heuristic (which covers minimax-m3, claude-3/4 families, gemini, - * gpt-4o/4.1, mistral-medium-3, and general "-vision" / "multimodal"). - */ -function isCommandCodeVisionModel(model?: string | null): boolean { - if (!model) return false; - // mimo-v2.5-pro is text-only — exclude before any positive check - if (/(?:^|\/)mimo-v2\.5-pro$/i.test(model)) return false; - // Only mimo-v2.5 and mimo-v2-omni accept images per Xiaomi vendor docs - if (/(?:^|\/)mimo-v2\.5$/i.test(model)) return true; - if (/(?:^|\/)mimo-v2-omni$/i.test(model)) return true; - // CC-specific patterns: Kimi K2, Qwen 3.x, Stepfun, Claude Fable, - // GPT-5, Sakana Fugu — not covered by the shared heuristic - if (CC_VISION_MODEL_PATTERNS.some((pattern) => pattern.test(model))) return true; - // Fall through: minimax-m3, claude-3/4, gemini-2/3, gpt-4o, -vision, multimodal - return isVisionModelId(model); -} - -/** - * Extract the image URL from an OpenAI-compatible or Command Code - * content part, returning undefined for non-image parts. - * - * OpenAI-compatible: { type: "image_url", image_url: { url: "..." } } - * Command Code CLI: { type: "image", image: "..." } - * AI SDK image: { type: "image", image: "data:...;base64,..." } (#1330) - * Anthropic image: { type: "image", source: { type: "base64", media_type, data } } - * or { type: "image", source: { type: "url", url } } - * - * The Anthropic-shaped block is common for Claude-Code-compatible clients - * (e.g. Zoo Code) that send Messages-style content arrays to the - * OpenAI `/v1/chat/completions` surface. Without this branch the image was - * silently dropped before reaching the upstream vision model. - */ -function extractImageUrl(part: JsonRecord): string | undefined { - if (part.type === "image") { - const direct = stringValue(part.image); - if (direct) return direct; - - // Anthropic source block: { source: { type: "base64", media_type, data } } or - // { source: { type: "url", url } }. - const source = isRecord(part.source) ? part.source : null; - if (source) { - if (source.type === "base64") { - const mediaType = stringValue(source.media_type) || "image/png"; - const data = stringValue(source.data); - if (data) return `data:${mediaType};base64,${data}`; - } - if (source.type === "url") { - const url = stringValue(source.url); - if (url) return url; - } - } - return undefined; - } - if (part.type === "image_url") { - if (isRecord(part.image_url)) return stringValue(part.image_url.url); - return stringValue(part.image_url); - } - return undefined; -} - -/** - * Convert an OpenAI-format content array to Command Code's internal - * CLI format. For vision-capable models (MiniMax M3, MiMo v2.5, etc.) - * this also preserves image parts alongside text. - */ -function convertUserContentParts(content: unknown, isVisionModel: boolean): string | unknown[] { - // For non-vision models or string content, extract text only. - if (!isVisionModel || typeof content === "string") { - return normalizeContentText(content); - } - - const parts: unknown[] = []; - for (const part of asRecordArray(content)) { - if (part.type === "text") { - const text = stringValue(part.text); - if (text) parts.push({ type: "text", text }); - continue; - } - const imgUrl = extractImageUrl(part); - if (imgUrl) { - parts.push({ type: "image", image: imgUrl }); - continue; - } - // Always drop tool_use / tool_result / thinking parts from user - // messages (Command Code doesn't accept them for role:"user"). - } - - // When every part was stripped, fall back to empty text so the - // message is still valid JSON (Command Code rejects empty content). - if (parts.length === 0) parts.push({ type: "text", text: "" }); - - return parts; -} - -function convertTools(tools: unknown, toolNameMap: Map): unknown[] { - return asRecordArray(tools).map((tool) => { - const fn = isRecord(tool.function) ? tool.function : tool; - return { - type: "function", - name: wireToolName(stringValue(fn.name) || "", toolNameMap), - description: stringValue(fn.description) || "", - input_schema: isRecord(fn.parameters) ? fn.parameters : {}, - }; - }); -} - -function buildToolCallMetadata( - messages: JsonRecord[], - toolNameMap: Map -): { - pairedToolCallIds: Set; - toolCallNames: Map; - toolCallArgs: Map; -} { - const callIds = new Set(); - const resultIds = new Set(); - const toolCallNames = new Map(); - const toolCallArgs = new Map(); - - for (const message of messages) { - if (message.role === "assistant") { - for (const call of asRecordArray(message.tool_calls)) { - const id = stringValue(call.id); - if (id) { - callIds.add(id); - const fn = isRecord(call.function) ? call.function : {}; - const name = stringValue(fn.name) || stringValue(call.name); - if (name) toolCallNames.set(id, wireToolName(name, toolNameMap)); - toolCallArgs.set(id, toolCallArgumentsString(fn.arguments)); - } - } - } else if (message.role === "tool") { - const id = stringValue(message.tool_call_id); - if (id) resultIds.add(id); - } - } - - const pairedToolCallIds = new Set([...callIds].filter((id) => resultIds.has(id))); - return { pairedToolCallIds, toolCallNames, toolCallArgs }; -} - -function convertMessages( - messages: unknown, - model?: string | null, - toolNameMap?: Map -): { system: string; messages: unknown[] } { - const source = asRecordArray(messages); - const { pairedToolCallIds, toolCallNames, toolCallArgs } = buildToolCallMetadata( - source, - toolNameMap ?? new Map() - ); - const out: unknown[] = []; - const system: string[] = []; - const isVision = isCommandCodeVisionModel(model); - - for (const message of source) { - const role = stringValue(message.role); - if (role === "system" || role === "developer") { - const text = normalizeContentText(message.content); - if (text) system.push(text); - continue; - } - - if (role === "user") { - out.push({ role: "user", content: convertUserContentParts(message.content, isVision) }); - continue; - } - - if (role === "assistant") { - const parts: unknown[] = []; - const text = normalizeContentText(message.content); - if (text) parts.push({ type: "text", text }); - - for (const call of asRecordArray(message.tool_calls)) { - const id = stringValue(call.id) || ""; - if (!id || !pairedToolCallIds.has(id)) continue; - const fn = isRecord(call.function) ? call.function : {}; - const parsedInput = recordOrEmpty(fn.arguments); - parts.push({ - type: "tool-call", - toolCallId: id, - toolName: wireToolName( - stringValue(fn.name) || stringValue(call.name) || "unknown", - toolNameMap ?? new Map() - ), - input: parsedInput, - // /alpha/generate requires this field on assistant tool-call parts; - // a missing one is rejected with `missing required field 'arguments'`. - arguments: toolCallArgumentsString(fn.arguments), - }); - } - - if (parts.length > 0) out.push({ role: "assistant", content: parts }); - continue; - } - - if (role === "tool") { - const toolCallId = stringValue(message.tool_call_id) || ""; - if (!toolCallId || !pairedToolCallIds.has(toolCallId)) continue; - const toolName = wireToolName( - stringValue(message.name) || toolCallNames.get(toolCallId) || "unknown", - toolNameMap ?? new Map() - ); - out.push({ - role: "tool", - content: [ - { - type: "tool-result", - toolCallId, - toolName, - // /alpha/generate requires `arguments` here too (same rejection as - // tool-call parts); echo the paired call's args, defensively "{}". - arguments: toolCallArgs.get(toolCallId) ?? "{}", - output: { type: "text", value: normalizeContentText(message.content) }, - }, - ], - }); - } - } - - return { system: system.join("\n\n"), messages: out }; -} - // Clamp a client-supplied max_tokens to the endpoint ceiling, mirroring the // provider-driven clamp in antigravity.ts: we only intervene when the value is -// present, positive AND would otherwise be rejected (> 200_000). A valid value -// is returned floored; anything absent, non-numeric or non-positive returns -// undefined so the caller can OMIT the field entirely and let Command Code's -// upstream apply the model's own native default (rather than us inventing a -// number). A non-positive value such as Zoo Code's max_tokens:-1 ("let the -// server choose") must be omitted, NOT forced to 1 — the old Math.max(1,...) -// truncated output to a single token (#5166). +// present, positive AND would otherwise be rejected (> MAX_COMMAND_CODE_TOKENS). +// A valid value is returned floored; anything absent, non-numeric or non-positive +// returns undefined so the caller can OMIT the field entirely and let the +// provider's upstream apply the model's own native default (rather than us +// inventing a number). A non-positive value such as Zoo Code's max_tokens:-1 +// ("let the server choose") must be omitted, NOT forced to 1 — the old +// Math.max(1,...) truncated output to a single token (#5166). function clampMaxTokens(value: unknown): number | undefined { const numeric = numberValue(value); if (numeric === undefined || numeric <= 0) return undefined; return Math.min(Math.floor(numeric), MAX_COMMAND_CODE_TOKENS); } -// Reasoning/thinking fields that payload rules or clients may inject and that -// CommandCode's upstream accepts inside `params`. Without this pass-through, -// payload-rule overrides on these fields are silently dropped (#2986 follow-up). -const COMMAND_CODE_PASSTHROUGH_FIELDS = [ - "reasoning_effort", - "reasoning", - "thinking", - "effort", - "output_config", - "extra_body", -] as const; - /** - * Command Code's /alpha/generate endpoint serves most models under a - * vendor-prefixed wire id (e.g. `xiaomi/mimo-v2.5`, `deepseek/deepseek-v4-pro`, - * `moonshotai/Kimi-K2.6`) and defaults an unprefixed id to the `anthropic:` - * provider, which 403s with "Model/provider not recognized: anthropic:". + * Command Code serves most models under a vendor-prefixed wire id (e.g. + * `xiaomi/mimo-v2.5`, `deepseek/deepseek-v4-pro`, `moonshotai/Kimi-K2.6`). * The command-code registry ids already carry the vendor prefix, so a bare id * reaching the executor is an operator-set custom model (e.g. the Vision Bridge * picker, #10809). Map the small set of documented bare ids to their * vendor-prefixed wire form; anything with an explicit `/` (or already wired) - * passes through untouched. Kept minimal and doc-backed, mirroring the - * `CC_VISION_MODEL_PATTERNS` philosophy. + * passes through untouched. Kept minimal and doc-backed. */ const COMMAND_CODE_BARE_MODEL_VENDOR_PREFIX: Readonly> = { - // Xiaomi MiMo V2.5 — the only CC-served vision model not in the registry. + // Xiaomi MiMo V2.5 — a CC-served vision model not in the registry. "mimo-v2.5": "xiaomi/mimo-v2.5", "mimo-v2.5-pro": "xiaomi/mimo-v2.5-pro", }; /** - * Normalize an incoming model id to the wire form Command Code's upstream + * Normalize an incoming model id to the wire form Command Code's provider API * accepts. Strips a leading provider prefix (`command-code/` / `cmd/`) that the * pipeline may have resolved, then maps known bare ids to their * vendor-prefixed form (see above). @@ -424,546 +68,47 @@ function normalizeCommandCodeWireModel(model: string): string { return COMMAND_CODE_BARE_MODEL_VENDOR_PREFIX[bare] ?? bare; } -function buildCommandCodeBody( +/** + * Build a flat OpenAI chat.completions request body for the official + * /provider/v1/chat/completions endpoint. The incoming body is already the + * standard OpenAI chat.completions shape (registry `format: "openai"`), so this + * is a passthrough that: normalizes the wire model id, forces the stream flag + * to match the caller's expectation, clamps max_tokens, and lets reasoning / + * payload-rule passthrough fields flow through untouched. No CLI envelope + * (config/memory/taste/skills/permissionMode) and no CLI-shaped message + * conversion here — /provider/v1 is the documented, standard API. + */ +function buildOpenAiBody( model: string, body: unknown, - stream = false -): { body: JsonRecord; toolNameMap: Map } { - const input = isRecord(body) ? body : {}; - const toolNameMap = new Map(); + stream: boolean +): { body: JsonRecord } { + const input = isRecord(body) ? { ...(body as JsonRecord) } : {}; - // Payload rules may rewrite `body.model` (e.g. deepseek-v4-pro-max → - // deepseek/deepseek-v4-pro for the command-code provider). Prefer the - // rewritten value if present; fall back to the resolved combo model arg. - // Normalize to the vendor-prefixed wire id the upstream requires (#10809). const resolvedModel = normalizeCommandCodeWireModel( - typeof input.model === "string" && input.model.trim().length > 0 ? input.model : model + typeof input.model === "string" && input.model.trim().length > 0 + ? input.model + : model ); - const converted = convertMessages(input.messages, resolvedModel, toolNameMap); - const explicitSystem = typeof input.system === "string" ? input.system : ""; - const system = [converted.system, explicitSystem].filter(Boolean).join("\n\n"); - - const params: JsonRecord = { + const out: JsonRecord = { + ...input, model: resolvedModel, - messages: converted.messages, - tools: convertTools(input.tools, toolNameMap), - system, - stream: true, + stream: stream === true, }; - // Only forward max_tokens when the client actually supplied one. Omitting it - // lets Command Code's upstream apply the model's own native default, so we - // never invent a value (the old behavior, which sent the wrong number and got - // DeepSeek V4 rejected with "Too big: expected number to be <=200000"). When - // present, it is clamped to the endpoint ceiling so an oversized client value - // degrades gracefully instead of 400ing. + // Forward max_tokens only when the client actually supplied a positive value + // (clamped to the endpoint ceiling). Omitting it lets the provider's upstream + // apply the model's own native default; a non-positive value such as -1 + // ("let the server choose") must be omitted, NOT coerced to 1 (#5166). const maxTokens = clampMaxTokens(input.max_tokens ?? input.max_completion_tokens); + delete out.max_tokens; + delete out.max_completion_tokens; if (maxTokens !== undefined) { - params.max_tokens = maxTokens; + out.max_tokens = maxTokens; } - for (const field of COMMAND_CODE_PASSTHROUGH_FIELDS) { - const value = input[field]; - if (value !== undefined && value !== null) { - params[field] = value; - } - } - - return { - body: { - config: { - workingDir: "/workspace", - date: new Date().toISOString().slice(0, 10), - environment: "external", - structure: [], - isGitRepo: false, - currentBranch: "", - mainBranch: "", - gitStatus: "", - recentCommits: [], - }, - memory: "", - taste: "", - skills: "", - permissionMode: "standard", - params, - }, - toolNameMap, - }; -} - -function parseStreamLine(line: string): unknown | undefined { - let trimmed = line.trim(); - if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:")) return undefined; - if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim(); - if (!trimmed || trimmed === "[DONE]") return undefined; - - try { - return JSON.parse(trimmed); - } catch (error) { - console.warn( - "[commandCode] stream line parse failed:", - error instanceof Error ? error.message : String(error) - ); - return undefined; - } -} - -function mapFinishReason(reason: unknown): "stop" | "length" | "tool_calls" { - if (reason === "tool-calls" || reason === "tool_calls" || reason === "toolUse") - return "tool_calls"; - if ( - reason === "length" || - reason === "max_tokens" || - reason === "max-tokens" || - reason === "max_output_tokens" - ) { - return "length"; - } - return "stop"; -} - -function chatCompletionChunk( - id: string, - model: string, - delta: JsonRecord, - finishReason: unknown = null -) { - return { - id, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model, - choices: [{ index: 0, delta, finish_reason: finishReason }], - }; -} - -function sse(data: unknown): Uint8Array { - return encoder.encode(`data: ${JSON.stringify(data)}\n\n`); -} - -type AggregateState = { - content: string; - reasoning: string; - toolCalls: JsonRecord[]; - finishReason: "stop" | "length" | "tool_calls"; - usage: JsonRecord | null; -}; - -function firstRecord(record: JsonRecord, keys: readonly string[]): JsonRecord { - for (const key of keys) { - const value = record[key]; - if (isRecord(value)) return value; - } - return {}; -} - -function firstNumber(record: JsonRecord, keys: readonly string[]): number | undefined { - for (const key of keys) { - const value = numberValue(record[key]); - if (value !== undefined) return value; - } - return undefined; -} - -/** Keep earlier finish-step usage when the terminal finish event omits it. */ -function mergeCommandCodeUsage(previous: JsonRecord | null, next: unknown): JsonRecord | null { - if (!isRecord(next)) return previous; - - const merged: JsonRecord = { ...(previous || {}), ...next }; - for (const key of [ - "inputTokenDetails", - "input_token_details", - "input_tokens_details", - "prompt_tokens_details", - "outputTokenDetails", - "output_token_details", - "output_tokens_details", - "completion_tokens_details", - "reasoningTokenDetails", - "reasoning_token_details", - ]) { - const before = isRecord(previous?.[key]) ? previous[key] : {}; - const after = isRecord(next[key]) ? next[key] : {}; - if (Object.keys(before).length > 0 || Object.keys(after).length > 0) { - merged[key] = { ...before, ...after }; - } - } - return merged; -} - -function rememberCommandCodeUsage(state: AggregateState, event: JsonRecord): void { - const usage = - event.type === "finish-step" - ? (event.usage ?? event.totalUsage) - : (event.totalUsage ?? event.usage); - state.usage = mergeCommandCodeUsage(state.usage, usage); -} - -function applyEventToAggregate( - event: JsonRecord, - state: AggregateState, - toolNameMap: Map -): void { - // Some Command Code protocol revisions attach usage to the terminal payload - // without preserving the event type. Capture it before event-specific handling. - rememberCommandCodeUsage(state, event); - - switch (event.type) { - case "text-delta": - state.content += stringValue(event.text) || ""; - break; - case "reasoning-delta": - state.reasoning += stringValue(event.text) || ""; - break; - case "tool-call": { - const args = recordOrEmpty(event.input ?? event.args ?? event.arguments); - state.toolCalls.push({ - id: stringValue(event.toolCallId) || stringValue(event.id) || randomUUID(), - type: "function", - function: { - name: clientToolName( - stringValue(event.toolName) || stringValue(event.name) || "", - toolNameMap - ), - arguments: JSON.stringify(args), - }, - }); - break; - } - case "finish-step": - break; - case "finish": - state.finishReason = mapFinishReason(event.finishReason); - break; - } -} - -function applyEventToAggregateOrThrow( - event: JsonRecord, - state: AggregateState, - toolNameMap: Map -): void { - if (event.type === "error") { - const error = isRecord(event.error) ? event.error : {}; - throw new Error( - stringValue(error.message) || stringValue(event.error) || "Command Code stream error" - ); - } - - applyEventToAggregate(event, state, toolNameMap); -} - -function usageFromCommandCode(usage: JsonRecord | null) { - if (!usage) return undefined; - const inputDetails = firstRecord(usage, [ - "inputTokenDetails", - "input_token_details", - "input_tokens_details", - "prompt_tokens_details", - ]); - const outputDetails = firstRecord(usage, [ - "outputTokenDetails", - "output_token_details", - "output_tokens_details", - "completion_tokens_details", - ]); - const reasoningDetails = firstRecord(usage, [ - "reasoningTokenDetails", - "reasoning_token_details", - "reasoning_tokens_details", - ]); - const cacheRead = - firstNumber(usage, [ - "cachedInputTokens", - "cached_input_tokens", - "cacheReadInputTokens", - "cache_read_input_tokens", - "cacheReadTokens", - "cache_read_tokens", - "cached_tokens", - ]) ?? - firstNumber(inputDetails, [ - "cachedTokens", - "cached_tokens", - "cacheReadTokens", - "cache_read_tokens", - ]); - const noCache = firstNumber(inputDetails, ["noCacheTokens", "no_cache_tokens"]); - // Command Code's totalUsage.inputTokens is the FULL prompt total and already - // includes the cached portion (noCacheTokens + cacheReadTokens = inputTokens), - // so we must NOT add cacheRead back — that would double-count. There is no - // cache-write field in the upstream payload, so cache creation stays unset. - const prompt = - firstNumber(usage, ["inputTokens", "input_tokens", "promptTokens", "prompt_tokens"]) ?? - (noCache ?? 0) + (cacheRead ?? 0); - const reasoning = - firstNumber(usage, ["reasoningTokens", "reasoning_tokens"]) ?? - firstNumber(outputDetails, ["reasoningTokens", "reasoning_tokens"]) ?? - firstNumber(reasoningDetails, ["reasoningTokens", "reasoning_tokens"]); - const textOutput = firstNumber(outputDetails, ["textTokens", "text_tokens"]); - const completion = - firstNumber(usage, [ - "outputTokens", - "output_tokens", - "completionTokens", - "completion_tokens", - ]) ?? (textOutput ?? 0) + (reasoning ?? 0); - const total = firstNumber(usage, ["totalTokens", "total_tokens"]) ?? prompt + completion; - const result: JsonRecord = { - prompt_tokens: prompt, - prompt_tokens_details: { cached_tokens: cacheRead ?? 0 }, - completion_tokens: completion, - completion_tokens_details: { reasoning_tokens: reasoning ?? 0 }, - total_tokens: total, - }; - // Surface the cache breakdown as informational fields so logUsage prints - // `| cache_read=X | no_cache=Y` and appendRequestLog persists them. These are - // NOT added to prompt_tokens (already included) — metering stays accurate. - if (cacheRead !== undefined && cacheRead > 0) result.cache_read_input_tokens = cacheRead; - if (noCache !== undefined && noCache > 0) result.no_cache_tokens = noCache; - if (reasoning !== undefined && reasoning > 0) result.reasoning_tokens = reasoning; - return result; -} - -function createStreamResponse( - upstream: Response, - model: string, - signal?: AbortSignal | null, - toolNameMap: Map = new Map() -): Response { - const id = `chatcmpl-${randomUUID()}`; - const reader = upstream.body?.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let sentRole = false; - let closed = false; - const state: AggregateState = { - content: "", - reasoning: "", - toolCalls: [], - finishReason: "stop", - usage: null, - }; - - const stream = new ReadableStream({ - start(controller) { - if (!reader) { - controller.error(new Error("Command Code response missing body")); - return; - } - - const abort = () => { - closed = true; - reader.cancel().catch(() => undefined); - controller.error(new DOMException("The operation was aborted", "AbortError")); - }; - signal?.addEventListener("abort", abort, { once: true }); - - const emitEvent = (event: unknown) => { - if (!isRecord(event) || closed) return; - rememberCommandCodeUsage(state, event); - if (!sentRole) { - sentRole = true; - controller.enqueue(sse(chatCompletionChunk(id, model, { role: "assistant" }))); - } - - switch (event.type) { - case "text-delta": { - const text = stringValue(event.text) || ""; - if (text) controller.enqueue(sse(chatCompletionChunk(id, model, { content: text }))); - state.content += text; - break; - } - case "reasoning-delta": { - const text = stringValue(event.text) || ""; - if (text) { - controller.enqueue(sse(chatCompletionChunk(id, model, { reasoning_content: text }))); - state.reasoning += text; - } - break; - } - case "tool-call": { - const index = state.toolCalls.length; - const args = recordOrEmpty(event.input ?? event.args ?? event.arguments); - const toolCall = { - id: stringValue(event.toolCallId) || stringValue(event.id) || randomUUID(), - type: "function", - function: { - name: clientToolName( - stringValue(event.toolName) || stringValue(event.name) || "", - toolNameMap - ), - arguments: JSON.stringify(args), - }, - }; - state.toolCalls.push(toolCall); - controller.enqueue( - sse(chatCompletionChunk(id, model, { tool_calls: [{ index, ...toolCall }] })) - ); - break; - } - case "reasoning-end": - break; - case "finish-step": - break; - case "finish": { - state.finishReason = mapFinishReason(event.finishReason); - controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason))); - // Emit a standards-compliant usage-only chunk (choices: []) before - // [DONE] when upstream reported usage. stream.ts's extractUsage - // recognizes this shape (see stream.ts:1661) and logs the ACTUAL - // token counts (in/out/cache_read/no_cache) instead of estimates. - const usagePayload = usageFromCommandCode(state.usage); - if (usagePayload) { - controller.enqueue( - sse({ - id, - object: "chat.completion.chunk", - model, - usage: usagePayload, - choices: [], - }) - ); - } - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - closed = true; - controller.close(); - reader.cancel().catch(() => undefined); - break; - } - case "error": { - const error = isRecord(event.error) ? event.error : {}; - throw new Error( - stringValue(error.message) || stringValue(event.error) || "Command Code stream error" - ); - } - } - }; - - const pump = async () => { - try { - for (;;) { - if (closed) return; - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) emitEvent(parseStreamLine(line)); - } - if (buffer.trim()) emitEvent(parseStreamLine(buffer)); - if (!closed) { - if (!sentRole) - controller.enqueue(sse(chatCompletionChunk(id, model, { role: "assistant" }))); - controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason))); - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - controller.close(); - } - } catch (error) { - controller.error(error); - } finally { - signal?.removeEventListener("abort", abort); - try { - reader.releaseLock(); - } catch (error) { - console.warn( - "[commandCode] reader releaseLock failed:", - error instanceof Error ? error.message : String(error) - ); - } - } - }; - - pump(); - }, - cancel() { - closed = true; - return reader?.cancel(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" }, - }); -} - -async function createJsonResponse( - upstream: Response, - model: string, - signal?: AbortSignal | null, - toolNameMap: Map = new Map() -): Promise { - const reader = upstream.body?.getReader(); - if (!reader) throw new Error("Command Code response missing body"); - - const decoder = new TextDecoder(); - let buffer = ""; - const state: AggregateState = { - content: "", - reasoning: "", - toolCalls: [], - finishReason: "stop", - usage: null, - }; - - try { - for (;;) { - if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError"); - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) { - const event = parseStreamLine(line); - if (!isRecord(event)) continue; - applyEventToAggregateOrThrow(event, state, toolNameMap); - } - } - if (buffer.trim()) { - const event = parseStreamLine(buffer); - if (isRecord(event)) applyEventToAggregateOrThrow(event, state, toolNameMap); - } - } finally { - try { - await reader.cancel(); - } catch (error) { - console.warn( - "[commandCode] reader cancel failed:", - error instanceof Error ? error.message : String(error) - ); - } - try { - reader.releaseLock(); - } catch (error) { - console.warn( - "[commandCode] reader releaseLock failed:", - error instanceof Error ? error.message : String(error) - ); - } - } - - const message: JsonRecord = { role: "assistant", content: state.content }; - if (state.reasoning) message.reasoning_content = state.reasoning; - if (state.toolCalls.length > 0) message.tool_calls = state.toolCalls; - - const payload: JsonRecord = { - id: `chatcmpl-${randomUUID()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model, - choices: [{ index: 0, message, finish_reason: state.finishReason }], - }; - const usage = usageFromCommandCode(state.usage); - if (usage) payload.usage = usage; - - return new Response(JSON.stringify(payload), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + return { body: out }; } export class CommandCodeExecutor extends BaseExecutor { @@ -973,7 +118,7 @@ export class CommandCodeExecutor extends BaseExecutor { buildUrl() { const baseUrl = (this.config.baseUrl || "https://api.commandcode.ai").replace(/\/$/, ""); - return `${baseUrl}${this.config.chatPath || "/alpha/generate"}`; + return `${baseUrl}${this.config.chatPath || "/provider/v1/chat/completions"}`; } async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) { @@ -983,26 +128,17 @@ export class CommandCodeExecutor extends BaseExecutor { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, - "x-command-code-version": COMMAND_CODE_VERSION, - "x-cli-environment": "external", - "x-project-slug": "pi-cc", - "x-taste-learning": "false", - "x-co-flag": "false", - "x-session-id": randomUUID(), + Accept: stream ? "text/event-stream" : "application/json", }; mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); // The combo/single-model dispatch boundary does not always run // sanitizeRequestForResolvedTarget before reaching this executor (combo // path), and Command Code rejects unsupported reasoning_effort values - // outright (e.g. "minimal" → 400 "expected one of low|medium|high|xhigh|max"). - // Sanitize here — the executor is the last line of defense for the wire body. + // outright. Sanitize here — the executor is the last line of defense for + // the wire body. const sanitizedBody = sanitizeReasoningEffortForProvider(body, this.provider, model); - const { body: transformedBody, toolNameMap } = buildCommandCodeBody( - model, - sanitizedBody, - stream - ); + const { body: transformedBody } = buildOpenAiBody(model, sanitizedBody, stream); const url = this.buildUrl(); const upstream = await fetch(url, { method: "POST", @@ -1028,10 +164,9 @@ export class CommandCodeExecutor extends BaseExecutor { }; } - const response = stream - ? createStreamResponse(upstream, model, signal, toolNameMap) - : await createJsonResponse(upstream, model, signal, toolNameMap); - - return { response, url, headers, transformedBody }; + // The /provider/v1/chat/completions endpoint returns standard OpenAI-format + // SSE (stream) or JSON (non-stream) straight through, so the upstream + // Response passes through untouched — no AI-SDK/CLI event re-parsing needed. + return { response: upstream, url, headers, transformedBody }; } -} +} \ No newline at end of file diff --git a/src/lib/providers/validation/openaiFormat.ts b/src/lib/providers/validation/openaiFormat.ts index 3d60113134..fa7f245579 100644 --- a/src/lib/providers/validation/openaiFormat.ts +++ b/src/lib/providers/validation/openaiFormat.ts @@ -1,7 +1,6 @@ // OpenAI/Gemini-format + Bedrock provider key validators (bedrock, openai-like, command-code, gemini-like, openai-compatible). // Extracted from validation.ts (god-file decomposition) — top-level functions; behavior is // byte-identical to the original inline defs. -import { randomUUID } from "node:crypto"; import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; import { discoverBedrockNativeModels, @@ -196,13 +195,12 @@ export async function validateOpenAILikeProvider({ export async function validateCommandCodeProvider({ apiKey, providerSpecificData = {} }: any) { const entry = getRegistryEntry("command-code"); const baseUrl = normalizeBaseUrl(entry?.baseUrl || "https://api.commandcode.ai"); - const chatPath = entry?.chatPath || "/alpha/generate"; + const chatPath = entry?.chatPath || "/provider/v1/chat/completions"; const url = `${baseUrl}${chatPath.startsWith("/") ? chatPath : `/${chatPath}`}`; const validationModelId = providerSpecificData?.validationModelId || entry?.models?.find((model) => model.id === "deepseek/deepseek-v4-flash")?.id || "deepseek/deepseek-v4-flash"; - const { COMMAND_CODE_VERSION } = await import("@omniroute/open-sse/executors/commandCode.ts"); return validateDirectChatProvider({ url, @@ -210,37 +208,13 @@ export async function validateCommandCodeProvider({ apiKey, providerSpecificData headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, - "x-command-code-version": COMMAND_CODE_VERSION, - "x-cli-environment": "external", - "x-project-slug": "pi-cc", - "x-taste-learning": "false", - "x-co-flag": "false", - "x-session-id": randomUUID(), + Accept: "text/event-stream", }, body: { - config: { - workingDir: "/workspace", - date: new Date().toISOString().slice(0, 10), - environment: "external", - structure: [], - isGitRepo: false, - currentBranch: "", - mainBranch: "", - gitStatus: "", - recentCommits: [], - }, - memory: "", - taste: "", - skills: "", - permissionMode: "standard", - params: { - model: validationModelId, - messages: [{ role: "user", content: "test" }], - tools: [], - system: "", - max_tokens: 1, - stream: true, - }, + model: validationModelId, + messages: [{ role: "user", content: "test" }], + stream: true, + max_tokens: 1, }, }); } diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 606d6eb493..5d87fb4705 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -83,7 +83,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = { textIcon: "CC", website: "https://commandcode.ai/", authHint: - "Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint.", + "Use a Command Code API key. Requests are sent to Command Code's /provider/v1/chat/completions endpoint.", apiHint: "Create or copy an API key from Command Code, then paste it here as a Bearer token.", }, openrouter: { diff --git a/tests/unit/command-code-executor.test.ts b/tests/unit/command-code-executor.test.ts index af1d317f5a..e9b305468d 100644 --- a/tests/unit/command-code-executor.test.ts +++ b/tests/unit/command-code-executor.test.ts @@ -8,20 +8,13 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-command-c process.env.DATA_DIR = TEST_DATA_DIR; const { REGISTRY, getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); -const { CommandCodeExecutor, COMMAND_CODE_VERSION } = - await import("../../open-sse/executors/commandCode.ts"); +const { CommandCodeExecutor } = await import("../../open-sse/executors/commandCode.ts"); const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); -const { createResponsesApiTransformStream } = - await import("../../open-sse/transformer/responsesTransformer.ts"); const core = await import("../../src/lib/db/core.ts"); const originalFetch = globalThis.fetch; -type JsonRecord = Record; -type ResponsesEvent = { - event: string; - data: { response: JsonRecord & { usage?: unknown; output?: JsonRecord[] } }; -}; +type FetchCall = { url: string; init: Record; body?: Record }; const PINNED_COMMAND_CODE_MODELS = [ "claude-opus-4-7", @@ -44,20 +37,7 @@ const PINNED_COMMAND_CODE_MODELS = [ "Qwen/Qwen3.6-Plus", ]; -function commandCodeStream(lines: unknown[], { sse = false } = {}) { - const text = lines - .map((line) => { - const json = JSON.stringify(line); - return sse ? `data: ${json}\n\n` : `${json}\n`; - }) - .join(""); - return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } }); -} - -function toPlainHeaders(headers: Headers | Record) { - if (headers instanceof Headers) return Object.fromEntries(headers.entries()); - return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)])); -} +const CHAT_URL = "https://api.commandcode.ai/provider/v1/chat/completions"; function parseSsePayloads(sse: string) { return sse @@ -68,25 +48,21 @@ function parseSsePayloads(sse: string) { .map((line) => JSON.parse(line)); } -async function responsesFromChatSse(sse: string): Promise { - const input = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(sse)); - controller.close(); - }, - }); - const transformed = await new Response( - input.pipeThrough(createResponsesApiTransformStream(null, 60_000)) - ).text(); +function openAiSse(obj: unknown): string { + return `data: ${JSON.stringify(obj)}\n\n`; +} - return transformed - .split("\n\n") - .map((part) => { - const event = part.match(/^event:\s*(.+)$/m)?.[1]; - const data = part.match(/^data:\s*(.+)$/m)?.[1]; - return event && data ? ({ event, data: JSON.parse(data) } as ResponsesEvent) : null; - }) - .filter((entry): entry is ResponsesEvent => entry !== null); +function captureFetch(body: Record) { + const calls: FetchCall[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ + url: String(url), + init, + body: JSON.parse(String(init.body)), + }); + return new Response(JSON.stringify(body), { status: 200 }); + }; + return calls; } test.afterEach(() => { @@ -105,7 +81,9 @@ test("Command Code provider catalog has pinned models and alias lookup", () => { assert.equal(entry.alias, "cmd"); assert.equal(entry.executor, "command-code"); assert.equal(entry.baseUrl, "https://api.commandcode.ai"); - assert.equal(entry.chatPath, "/alpha/generate"); + // Chat targets the documented /provider/v1/chat/completions endpoint, NOT the + // CLI-only /alpha/generate endpoint (#10265). + assert.equal(entry.chatPath, "/provider/v1/chat/completions"); assert.deepEqual( entry.models.map((model) => model.id), PINNED_COMMAND_CODE_MODELS @@ -119,17 +97,10 @@ test("getExecutor returns the specialized Command Code executor", () => { assert.ok(getExecutor("cmd") instanceof CommandCodeExecutor); }); -type FetchCall = { url: string; init: Record; body?: unknown }; - -test("Command Code executor posts wrapped body and required headers to /alpha/generate", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init }); - return commandCodeStream([{ type: "text-delta", text: "hello" }, { type: "finish" }]); - }; - +test("Command Code executor posts a flat OpenAI body + standard headers to /provider/v1/chat/completions (#10265)", async () => { + const calls = captureFetch({}); const executor = getExecutor("command-code"); - const { response, url, headers, transformedBody } = await executor.execute({ + const { response, url, headers } = await executor.execute({ model: "gpt-5.4-mini", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -144,41 +115,34 @@ test("Command Code executor posts wrapped body and required headers to /alpha/ge }, }); - assert.equal(url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(url, CHAT_URL); assert.equal(calls.length, 1); - assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(calls[0].url, CHAT_URL); assert.equal(calls[0].init.method, "POST"); assert.equal(headers.Authorization, "Bearer cc_test_key"); - assert.equal(headers["x-command-code-version"], COMMAND_CODE_VERSION); - assert.equal(headers["x-cli-environment"], "external"); - assert.equal(headers["x-project-slug"], "pi-cc"); - assert.equal(headers["x-taste-learning"], "false"); - assert.equal(headers["x-co-flag"], "false"); - assert.equal(typeof headers["x-session-id"], "string"); + // No CLI-impersonation headers. + assert.equal(headers["x-command-code-version"], undefined); + assert.equal(headers["x-cli-environment"], undefined); + assert.equal(headers["x-project-slug"], undefined); - const posted = JSON.parse(String(calls[0].init.body)); - assert.deepEqual(posted, transformedBody); - for (const key of ["config", "memory", "taste", "skills", "permissionMode", "params"]) { - assert.ok(key in posted, `missing ${key}`); - } - assert.equal(posted.skills, ""); - assert.equal(posted.params.model, "gpt-5.4-mini"); - assert.equal(posted.params.stream, true); - assert.equal(posted.params.system, "You are concise."); - assert.equal(posted.params.messages[0].role, "user"); - assert.equal(posted.params.tools[0].name, "lookup"); + const posted = calls[0].body as Record; + // No CLI envelope. + assert.equal(posted.config, undefined, "CLI envelope config must not be sent"); + assert.equal(posted.params, undefined, "CLI envelope params wrapper must not be sent"); + assert.equal(posted.model, "gpt-5.4-mini"); + assert.equal(posted.stream, false); + assert.equal((posted.messages as Array<{ role: string }>)[0].role, "system"); + const tool = (posted.tools as Array<{ function: { name: string } }>)[0]; + assert.equal(tool.function.name, "lookup", "tools in OpenAI shape (function.name)"); + assert.equal(posted.max_tokens, 42); + // The upstream OpenAI JSON passes through untouched. const json = await response.json(); - assert.equal(json.choices[0].message.content, "hello"); + assert.deepEqual(json, {}); }); -test("Command Code executor passes reasoning/thinking fields through to params (#2986 follow-up)", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - +test("Command Code executor passes reasoning/thinking fields through at the top level of the OpenAI body", async () => { + const calls = captureFetch({}); await getExecutor("command-code").execute({ model: "deepseek/deepseek-v4-pro", stream: false, @@ -189,30 +153,19 @@ test("Command Code executor passes reasoning/thinking fields through to params ( reasoning_effort: "high", thinking: { type: "enabled" }, effort: "high", - output_config: { effort: "high" }, extra_body: { enable_thinking: true }, }, }); - const posted = JSON.parse(String(calls[0].init.body)); - assert.equal(posted.params.reasoning_effort, "high"); - assert.deepEqual(posted.params.thinking, { type: "enabled" }); - assert.equal(posted.params.effort, "high"); - assert.deepEqual(posted.params.output_config, { effort: "high" }); - assert.deepEqual(posted.params.extra_body, { enable_thinking: true }); + const posted = calls[0].body as Record; + assert.equal(posted.reasoning_effort, "high"); + assert.deepEqual(posted.thinking, { type: "enabled" }); + assert.equal(posted.effort, "high"); + assert.deepEqual(posted.extra_body, { enable_thinking: true }); }); test("Command Code executor honors body.model rewrite from payload rules", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - - // Simulate a payload-rule rewrite: combo resolves to "deepseek-v4-pro-max" - // (passed as the execute() model arg), but the payload rule overwrites - // body.model to "deepseek/deepseek-v4-pro" (the vendor-prefixed form - // Command Code's API expects). + const calls = captureFetch({}); await getExecutor("command-code").execute({ model: "deepseek-v4-pro-max", stream: false, @@ -225,20 +178,13 @@ test("Command Code executor honors body.model rewrite from payload rules", async }, }); - const posted = JSON.parse(String(calls[0].init.body)); - assert.equal(posted.params.model, "deepseek/deepseek-v4-pro"); - assert.equal(posted.params.reasoning_effort, "max"); + const posted = calls[0].body as Record; + assert.equal(posted.model, "deepseek/deepseek-v4-pro"); + assert.equal(posted.reasoning_effort, "max"); }); test("Command Code executor maps unsupported minimal reasoning_effort to low (upstream 400 regression)", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - - // Live upstream rejection: "Validation error: Invalid option: expected one of - // \"low\"|\"medium\"|\"high\"|\"xhigh\"|\"max\" at \"params.reasoning_effort\"" — + const calls = captureFetch({}); // `minimal` (a Muse Spark catalog tier) must be downgraded to `low` before // the wire body is built, on BOTH the combo and single-model paths. await getExecutor("command-code").execute({ @@ -252,20 +198,38 @@ test("Command Code executor maps unsupported minimal reasoning_effort to low (up }, }); - const posted = JSON.parse(String(calls[0].init.body)); - assert.equal(posted.params.reasoning_effort, "low", "minimal must map to low"); + const posted = calls[0].body as Record; + assert.equal(posted.reasoning_effort, "low", "minimal must map to low"); }); -test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () => { - const calls: FetchCall[] = []; +test("Command Code executor passes the upstream OpenAI SSE stream through untouched", async () => { + const sse = + openAiSse({ + id: "c1", + object: "chat.completion.chunk", + model: "gpt-5.4", + choices: [{ index: 0, delta: { role: "assistant" } }], + }) + + openAiSse({ + id: "c1", + object: "chat.completion.chunk", + model: "gpt-5.4", + choices: [{ index: 0, delta: { content: "Hello" } }], + }) + + openAiSse({ + id: "c1", + object: "chat.completion.chunk", + model: "gpt-5.4", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }) + + "data: [DONE]\n\n"; + let capturedStreamFlag: unknown = null; globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); - return commandCodeStream([ - { type: "text-delta", text: "Hello" }, - { type: "reasoning-delta", text: "thinking" }, - { type: "tool-call", toolCallId: "call_1", toolName: "search", input: { q: "docs" } }, - { type: "finish", finishReason: "tool-calls" }, - ]); + capturedStreamFlag = JSON.parse(String(init.body)).stream; + return new Response(sse, { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }); }; const { response } = await getExecutor("command-code").execute({ @@ -275,39 +239,32 @@ test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () = body: { messages: [{ role: "user", content: "Hi" }] }, }); - assert.equal(calls[0].body.params.stream, true); - assert.equal(response.headers.get("Content-Type"), "text/event-stream; charset=utf-8"); - const sse = await response.text(); - assert.match(sse, /data: \[DONE\]/); - const chunks = parseSsePayloads(sse); - assert.equal(chunks[0].object, "chat.completion.chunk"); - assert.deepEqual(chunks[0].choices[0].delta, { role: "assistant" }); + assert.equal(capturedStreamFlag, true, "stream flag forwarded to upstream"); + const text = await response.text(); + assert.equal(text, sse, "OpenAI SSE stream passed through byte-for-byte"); + assert.ok(text.includes("data: [DONE]")); + const chunks = parseSsePayloads(text); + assert.equal(chunks[0].choices[0].delta.role, "assistant"); assert.equal(chunks[1].choices[0].delta.content, "Hello"); - assert.equal(chunks[2].choices[0].delta.reasoning_content, "thinking"); - assert.equal(chunks[3].choices[0].delta.tool_calls[0].function.name, "search"); - assert.equal(chunks.at(-1).choices[0].finish_reason, "tool_calls"); + assert.equal(chunks[2].choices[0].finish_reason, "stop"); }); -test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON", async () => { - globalThis.fetch = async () => - commandCodeStream( - [ - { type: "text-delta", text: "Hel" }, - { type: "text-delta", text: "lo" }, - { type: "reasoning-delta", text: "because" }, - { type: "tool-call", id: "call_2", name: "lookup", arguments: { id: 7 } }, - { - type: "finish", - finishReason: "max_tokens", - totalUsage: { - inputTokens: 3, - inputTokenDetails: { cacheReadTokens: 2 }, - outputTokens: 5, - }, - }, - ], - { sse: true } - ); +test("Command Code executor passes the upstream OpenAI JSON through untouched (non-stream)", async () => { + const upstreamJson = { + id: "chatcmpl-1", + object: "chat.completion", + model: "gpt-5.4-mini", + choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }; + let capturedStreamFlag: unknown = null; + globalThis.fetch = async (url, init = {}) => { + capturedStreamFlag = JSON.parse(String(init.body)).stream; + return new Response(JSON.stringify(upstreamJson), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; const { response } = await getExecutor("command-code").execute({ model: "gpt-5.4-mini", @@ -316,26 +273,12 @@ test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON body: { messages: [{ role: "user", content: "Hi" }] }, }); - assert.equal(response.headers.get("Content-Type"), "application/json"); - const json = await response.json(); - assert.equal(json.object, "chat.completion"); - assert.equal(json.choices[0].message.content, "Hello"); - assert.equal(json.choices[0].message.reasoning_content, "because"); - assert.equal(json.choices[0].message.tool_calls[0].function.arguments, JSON.stringify({ id: 7 })); - assert.equal(json.choices[0].finish_reason, "length"); - assert.deepEqual(json.usage, { - prompt_tokens: 3, - prompt_tokens_details: { cached_tokens: 2 }, - completion_tokens: 5, - completion_tokens_details: { reasoning_tokens: 0 }, - total_tokens: 8, - cache_read_input_tokens: 2, - }); + assert.equal(capturedStreamFlag, false, "stream flag forwarded as false for non-stream"); + assert.deepEqual(await response.json(), upstreamJson); }); -test("Command Code executor surfaces upstream and streamed errors", async () => { - globalThis.fetch = async () => - new Response("bad key", { status: 401, statusText: "Unauthorized" }); +test("Command Code executor surfaces upstream errors", async () => { + globalThis.fetch = async () => new Response("bad key", { status: 401, statusText: "Unauthorized" }); const upstreamFailure = await getExecutor("command-code").execute({ model: "gpt-5.4-mini", stream: false, @@ -344,124 +287,68 @@ test("Command Code executor surfaces upstream and streamed errors", async () => }); assert.equal(upstreamFailure.response.status, 401); assert.equal(await upstreamFailure.response.text(), "bad key"); - - globalThis.fetch = async () => commandCodeStream([{ type: "error", error: { message: "boom" } }]); - await assert.rejects(async () => { - await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - }, /boom/); }); -test("Command Code executor omits max_tokens when the client does not supply one (GLM-5.x)", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - - // No client max_tokens: we must NOT fabricate one. Omitting the field lets - // Command Code's upstream apply the model's own native default. +test("Command Code executor omits max_tokens when the client does not supply one", async () => { + const calls = captureFetch({}); await getExecutor("command-code").execute({ model: "zai-org/GLM-5.1", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Hi" }] }, }); - assert.ok(!("max_tokens" in calls[0].body.params)); -}); - -test("Command Code executor omits max_tokens for DeepSeek v4 when the client does not supply one", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - - // Regression: previously the executor invented max_tokens from the registry - // (384000), which /alpha/generate rejects with a 400 - // "Too big: expected number to be <=200000". With no client value we now omit - // the field entirely, so the request succeeds and upstream picks the default. - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - assert.ok(!("max_tokens" in calls[0].body.params)); + const posted = calls[0].body as Record; + assert.ok(!("max_tokens" in posted), "must not fabricate max_tokens"); + assert.ok(!("max_completion_tokens" in posted), "must not fabricate max_completion_tokens"); }); test("Command Code executor clamps an oversized client-supplied max_tokens to the endpoint ceiling", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - - // A client asking for more than the 200000 endpoint ceiling is clamped down - // (not 400'd), mirroring the provider-driven clamp in antigravity.ts. + const calls = captureFetch({}); + // A client asking for more than the 200000 endpoint ceiling is clamped down. await getExecutor("command-code").execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Hi" }], max_tokens: 500000 }, }); - assert.equal(calls[0].body.params.max_tokens, 200000); + assert.equal((calls[0].body as Record).max_tokens, 200000); }); -test("Command Code executor honors a smaller client-provided max_tokens under the per-model cap", async () => { - const calls: FetchCall[] = []; - globalThis.fetch = async (url, init = {}) => { - calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); - return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); - }; - +test("Command Code executor honors a smaller client-provided max_tokens", async () => { + const calls = captureFetch({}); await getExecutor("command-code").execute({ model: "zai-org/GLM-5.1", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Hi" }], max_tokens: 2048 }, }); - assert.equal(calls[0].body.params.max_tokens, 2048); + assert.equal((calls[0].body as Record).max_tokens, 2048); }); -test("Command Code non-stream aggregation throws when the final error event lacks a trailing newline", async () => { - globalThis.fetch = async () => - new Response( - `${JSON.stringify({ type: "text-delta", text: "Hello" })}\n${JSON.stringify({ - type: "error", - error: { message: "boom" }, - })}`, - { status: 200, headers: { "Content-Type": "application/x-ndjson" } } - ); - - await assert.rejects(async () => { - await getExecutor("command-code").execute({ +test("Command Code stream preserves the upstream OpenAI usage chunk (passthrough)", async () => { + const sse = + openAiSse({ + id: "c1", + object: "chat.completion.chunk", model: "gpt-5.4-mini", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - }, /boom/); -}); - -test("Command Code usage chunk surfaces cache_read and no_cache for the stream pipeline", async () => { - globalThis.fetch = async () => - commandCodeStream([ - { type: "text-delta", text: "Hi" }, - { - type: "finish", - finishReason: "stop", - totalUsage: { - inputTokens: 10, - inputTokenDetails: { noCacheTokens: 6, cacheReadTokens: 4 }, - outputTokens: 6, - }, + choices: [{ index: 0, delta: { content: "Hi" } }], + }) + + openAiSse({ + id: "c1", + object: "chat.completion.chunk", + model: "gpt-5.4-mini", + choices: [], + usage: { + prompt_tokens: 10, + prompt_tokens_details: { cached_tokens: 4 }, + completion_tokens: 6, + completion_tokens_details: { reasoning_tokens: 1 }, + total_tokens: 16, }, - ]); + }) + + "data: [DONE]\n\n"; + globalThis.fetch = async () => + new Response(sse, { status: 200, headers: { "Content-Type": "text/event-stream" } }); const { response } = await getExecutor("command-code").execute({ model: "gpt-5.4-mini", @@ -470,260 +357,11 @@ test("Command Code usage chunk surfaces cache_read and no_cache for the stream p body: { messages: [{ role: "user", content: "Hi" }] }, }); - const sse = await response.text(); - const chunks = parseSsePayloads(sse); - const usageChunk = chunks.find( - (chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0 - ); - assert.ok(usageChunk, "expected a usage-only chunk (choices: []) in the stream"); - - // The usage-only chunk feeds stream.ts's extractUsage, which surfaces - // cache_read_input_tokens / no_cache_tokens into the [USAGE] line. - const { extractUsage } = await import("../../open-sse/utils/usageTracking.ts"); - const extracted = extractUsage(usageChunk); - assert.ok(extracted, "extractUsage should recognize the usage-only chunk"); - assert.equal(extracted.prompt_tokens, 10); - assert.equal(extracted.completion_tokens, 6); - assert.equal(extracted.cache_read_input_tokens, 4); - assert.equal(extracted.no_cache_tokens, 6); -}); - -test("Command Code stream emits a usage-only chunk with actual tokens before [DONE]", async () => { - globalThis.fetch = async () => - commandCodeStream([ - { type: "text-delta", text: "Hi" }, - { - type: "finish", - finishReason: "stop", - totalUsage: { - inputTokens: 10, - inputTokenDetails: { cacheReadTokens: 4, cacheCreationTokens: 2 }, - outputTokens: 6, - reasoningTokenDetails: { reasoningTokens: 1 }, - }, - }, - ]); - - const { response } = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: true, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - - const sse = await response.text(); - const chunks = parseSsePayloads(sse); - - // Find the usage-only chunk: choices must be [] and usage must carry the - // actual upstream numbers. prompt_tokens = inputTokens (10) — cacheRead 4 is - // already included in that 10, so it is reported separately, NOT re-added. - const usageChunk = chunks.find( - (chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0 - ); - assert.ok(usageChunk, "expected a usage-only chunk (choices: []) in the stream"); - assert.deepEqual(usageChunk.usage, { - prompt_tokens: 10, - prompt_tokens_details: { cached_tokens: 4 }, - completion_tokens: 6, - completion_tokens_details: { reasoning_tokens: 1 }, - total_tokens: 16, - cache_read_input_tokens: 4, - reasoning_tokens: 1, - }); - // The usage chunk must come before the [DONE] marker. - assert.match(sse, /"usage":/); - const doneIndex = sse.indexOf("data: [DONE]"); - const usageIndex = sse.indexOf(`"choices":[]`); - assert.ok(usageIndex > -1 && usageIndex < doneIndex, "usage chunk must precede [DONE]"); -}); - -test("Command Code non-stream usage keeps inputTokens as prompt_tokens and reports cache separately", async () => { - globalThis.fetch = async () => - commandCodeStream( - [ - { type: "text-delta", text: "ok" }, - { - type: "finish", - finishReason: "stop", - totalUsage: { - inputTokens: 5, - inputTokenDetails: { noCacheTokens: 2, cacheReadTokens: 3 }, - outputTokens: 2, - }, - }, - ], - { sse: true } - ); - - const { response } = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - - const json = await response.json(); - assert.deepEqual(json.usage, { - prompt_tokens: 5, - prompt_tokens_details: { cached_tokens: 3 }, - completion_tokens: 2, - completion_tokens_details: { reasoning_tokens: 0 }, - total_tokens: 7, - cache_read_input_tokens: 3, - no_cache_tokens: 2, - }); -}); - -test("Command Code preserves finish-step usage through a finish without totalUsage", async () => { - globalThis.fetch = async () => - commandCodeStream([ - { type: "text-delta", text: "Hi" }, - { - type: "finish-step", - usage: { - inputTokens: 7308, - inputTokenDetails: { noCacheTokens: 27, cacheReadTokens: 7281 }, - outputTokens: 177, - outputTokenDetails: { textTokens: 12, reasoningTokens: 165 }, - totalTokens: 7485, - }, - }, - { type: "finish", finishReason: "stop", totalUsage: null }, - ]); - - const { response } = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: true, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - const sse = await response.text(); - const chunks = parseSsePayloads(sse); - const usageChunk = chunks.find( - (chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0 - ); - - assert.deepEqual(usageChunk?.usage, { - prompt_tokens: 7308, - prompt_tokens_details: { cached_tokens: 7281 }, - completion_tokens: 177, - completion_tokens_details: { reasoning_tokens: 165 }, - total_tokens: 7485, - cache_read_input_tokens: 7281, - no_cache_tokens: 27, - reasoning_tokens: 165, - }); - - const completed = (await responsesFromChatSse(sse)).find( - (event) => event.event === "response.completed" - ); - assert.deepEqual(completed?.data.response.usage, { - input_tokens: 7308, - input_tokens_details: { cached_tokens: 7281 }, - output_tokens: 177, - output_tokens_details: { reasoning_tokens: 165 }, - total_tokens: 7485, - }); -}); - -test("Command Code accepts OpenAI-style usage aliases with absent optional details", async () => { - globalThis.fetch = async () => - commandCodeStream([ - { - type: "finish-step", - usage: { - prompt_tokens: 11, - prompt_tokens_details: { cached_tokens: 4 }, - completion_tokens: 5, - completion_tokens_details: { reasoning_tokens: 2 }, - total_tokens: 16, - }, - }, - { type: "finish", finishReason: "stop" }, - ]); - - const { response } = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: true, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - const sse = await response.text(); - const usageChunk = parseSsePayloads(sse).find( - (chunk) => Array.isArray(chunk.choices) && chunk.choices.length === 0 - ); - assert.deepEqual(usageChunk?.usage, { - prompt_tokens: 11, - prompt_tokens_details: { cached_tokens: 4 }, - completion_tokens: 5, - completion_tokens_details: { reasoning_tokens: 2 }, - total_tokens: 16, - cache_read_input_tokens: 4, - reasoning_tokens: 2, - }); - - globalThis.fetch = async () => - commandCodeStream([ - { type: "finish-step", usage: { inputTokens: 4, outputTokens: 3, totalTokens: 7 } }, - { type: "finish", finishReason: "stop", totalUsage: null }, - ]); - const fallback = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: true, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - const fallbackSse = await fallback.response.text(); - const completed = (await responsesFromChatSse(fallbackSse)).find( - (event) => event.event === "response.completed" - ); - assert.deepEqual(completed?.data.response.usage, { - input_tokens: 4, - input_tokens_details: { cached_tokens: 0 }, - output_tokens: 3, - output_tokens_details: { reasoning_tokens: 0 }, - total_tokens: 7, - }); -}); - -test("Command Code preserves tool-call streaming while finalizing finish-step usage", async () => { - globalThis.fetch = async () => - commandCodeStream([ - { - type: "tool-call", - toolCallId: "call_1", - toolName: "lookup", - input: { query: "hello" }, - }, - { type: "finish-step", usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 } }, - { type: "finish", finishReason: "tool-calls" }, - ]); - - const { response } = await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: true, - credentials: { apiKey: "cc_test_key" }, - body: { messages: [{ role: "user", content: "Hi" }] }, - }); - const sse = await response.text(); - assert.ok( - parseSsePayloads(sse).some( - (chunk) => chunk.choices?.[0]?.delta?.tool_calls?.[0]?.id === "call_1" - ) - ); - - const completed = (await responsesFromChatSse(sse)).find( - (event) => event.event === "response.completed" - ); - assert.equal( - completed?.data.response.output?.some((item) => item.type === "function_call"), - true - ); - assert.deepEqual(completed?.data.response.usage, { - input_tokens: 3, - input_tokens_details: { cached_tokens: 0 }, - output_tokens: 2, - output_tokens_details: { reasoning_tokens: 0 }, - total_tokens: 5, - }); -}); + const text = await response.text(); + // The upstream OpenAI usage chunk passes through unchanged, including the + // standard OpenAI usage shape the stream pipeline already understands. + assert.ok(text.includes('"prompt_tokens":10')); + assert.ok(text.includes('"cached_tokens":4')); + assert.ok(text.includes('"reasoning_tokens":1')); + assert.ok(text.includes("data: [DONE]")); +}); \ No newline at end of file diff --git a/tests/unit/command-code-maxtokens-negative-5166.test.ts b/tests/unit/command-code-maxtokens-negative-5166.test.ts index e155aa72f0..2e30975b99 100644 --- a/tests/unit/command-code-maxtokens-negative-5166.test.ts +++ b/tests/unit/command-code-maxtokens-negative-5166.test.ts @@ -34,7 +34,7 @@ test.after(() => { core.resetDbInstance(); }); -async function captureParams(body: Record): Promise { +async function captureBody(body: Record): Promise { const calls: FetchCall[] = []; globalThis.fetch = async (url: any, init: any = {}) => { calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); @@ -50,27 +50,27 @@ async function captureParams(body: Record): Promise } test("Command Code omits max_tokens when the client sends max_tokens: -1 (#5166)", async () => { - const call = await captureParams({ max_tokens: -1 }); + const call = await captureBody({ max_tokens: -1 }); assert.ok( - !("max_tokens" in call.body.params), - `max_tokens:-1 must be omitted, got params.max_tokens=${call.body.params.max_tokens}` + !("max_tokens" in call.body), + `max_tokens:-1 must be omitted, got max_tokens=${call.body.max_tokens}` ); }); test("Command Code omits max_tokens when the client sends max_completion_tokens: -1 (#5166)", async () => { - const call = await captureParams({ max_completion_tokens: -1 }); + const call = await captureBody({ max_completion_tokens: -1 }); assert.ok( - !("max_tokens" in call.body.params), - `max_completion_tokens:-1 must be omitted, got params.max_tokens=${call.body.params.max_tokens}` + !("max_tokens" in call.body), + `max_completion_tokens:-1 must be omitted, got max_tokens=${call.body.max_tokens}` ); }); test("Command Code omits max_tokens when the client sends 0 (#5166)", async () => { - const call = await captureParams({ max_tokens: 0 }); - assert.ok(!("max_tokens" in call.body.params), "max_tokens:0 must be omitted"); + const call = await captureBody({ max_tokens: 0 }); + assert.ok(!("max_tokens" in call.body), "max_tokens:0 must be omitted"); }); test("Command Code still honors a positive client max_tokens after the #5166 fix", async () => { - const call = await captureParams({ max_tokens: 2048 }); - assert.equal(call.body.params.max_tokens, 2048); -}); + const call = await captureBody({ max_tokens: 2048 }); + assert.equal(call.body.max_tokens, 2048); +}); \ No newline at end of file diff --git a/tests/unit/command-code-user-array-5166.test.ts b/tests/unit/command-code-user-array-5166.test.ts index 01d20a15ff..a692012ed0 100644 --- a/tests/unit/command-code-user-array-5166.test.ts +++ b/tests/unit/command-code-user-array-5166.test.ts @@ -1,14 +1,13 @@ /** - * Regression test for #5166 (user-content-array 400 on Command Code / deepseek-v4-pro). + * #5166 (user-content-array 400 on Command Code / deepseek-v4-pro) context. * - * When a client sends a user message whose `content` is an array of content parts - * (e.g. [{type:"text",text:"Hello"},{type:"text",text:"World"}]), the raw array - * must NOT reach the Command Code upstream — it requires user content to be a plain - * string. The executor must normalise the array to a string before posting. - * - * NOTE: this file covers ONLY the user-content-array/400 symptom of #5166. - * The 0-output-token symptom on mimo-v2.5-pro (reasoning-only models) is tracked - * separately and is NOT addressed here. + * The original regression was that a user message whose `content` was an array of + * content parts reached the CLI-only /alpha/generate endpoint, which required + * user content to be a plain string. Since #10265 the executor posts to the + * documented /provider/v1/chat/completions endpoint, which natively speaks the + * OpenAI chat.completions format — array content (text + image_url parts) is + * valid there and passes through unchanged. These tests pin that OpenAI-shaped + * passthrough. */ import test from "node:test"; import assert from "node:assert/strict"; @@ -26,9 +25,8 @@ const core = await import("../../src/lib/db/core.ts"); const originalFetch = globalThis.fetch; -function commandCodeStream(lines: unknown[]) { - const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n"; - return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } }); +function okResponse() { + return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }); } test.after(() => { @@ -41,155 +39,100 @@ test.afterEach(() => { globalThis.fetch = originalFetch; }); -// ── helpers ──────────────────────────────────────────────────────────────────── +// ── helpers ──────────────────────────────────────────────────────────── type FetchCall = { url: string; init: Record; body: Record }; function captureFetch(response: Response) { const calls: FetchCall[] = []; globalThis.fetch = async (url, init: RequestInit = {}) => { - calls.push({ url: String(url), init: init as Record, body: JSON.parse(String(init.body)) }); + calls.push({ + url: String(url), + init: init as Record, + body: JSON.parse(String(init.body)), + }); return response; }; return calls; } -// ── failing tests (before fix, user content is the raw array) ────────────── +test("#5166 user message with multi-part array content passes through as an OpenAI array", async () => { + const calls = captureFetch(okResponse()); + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Hello" }, + { type: "text", text: "World" }, + ], + }, + ], + }, + }); -test( - "#5166 user message with multi-part array content is flattened to a string (#5166)", - async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); + const userMsg = (calls[0].body.messages as Record[])[0]; + // OpenAI array content is valid on /provider/v1 — forwarded as-is. + assert.ok(Array.isArray(userMsg.content), "array content forwarded (no CLI flattening)"); + const parts = userMsg.content as Record[]; + assert.equal(parts.length, 2); + assert.equal(parts[0].text, "Hello"); + assert.equal(parts[1].text, "World"); +}); - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Hello" }, - { type: "text", text: "World" }, - ], - }, - ], - }, - }); +test("#5166 user message with single text-part array passes through", async () => { + const calls = captureFetch(okResponse()); + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + messages: [{ role: "user", content: [{ type: "text", text: "Hi there" }] }], + }, + }); + const userMsg = (calls[0].body.messages as Record[])[0]; + const parts = userMsg.content as Record[]; + assert.equal(parts.length, 1); + assert.equal(parts[0].text, "Hi there"); +}); - const posted = calls[0].body; - const userMsg = (posted.params as Record).messages[0] as Record< - string, - unknown - >; +test("#5166 user message with plain string content passes through unchanged", async () => { + const calls = captureFetch(okResponse()); + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Plain string message" }] }, + }); + const userMsg = (calls[0].body.messages as Record[])[0]; + assert.equal(userMsg.content, "Plain string message"); +}); - // Must be a string — never an array — otherwise Command Code's upstream returns 400. - assert.equal( - typeof userMsg.content, - "string", - `user message content must be a string, got ${typeof userMsg.content}` - ); - // Joined text parts with "\n" - assert.equal(userMsg.content, "Hello\nWorld"); - } -); - -test( - "#5166 user message with single text-part array is flattened to a plain string", - async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [{ type: "text", text: "Hi there" }], - }, - ], - }, - }); - - const posted = calls[0].body; - const userMsg = (posted.params as Record).messages[0] as Record< - string, - unknown - >; - assert.equal(typeof userMsg.content, "string"); - assert.equal(userMsg.content, "Hi there"); - } -); - -test( - "#5166 user message with plain string content passes through unchanged (no regression)", - async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: "Plain string message", - }, - ], - }, - }); - - const posted = calls[0].body; - const userMsg = (posted.params as Record).messages[0] as Record< - string, - unknown - >; - assert.equal(typeof userMsg.content, "string"); - assert.equal(userMsg.content, "Plain string message"); - } -); - -test( - "#5166 user message with mixed parts (text + image_url) keeps only text parts", - async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Describe this:" }, - { type: "image_url", image_url: { url: "https://example.com/img.png" } }, - ], - }, - ], - }, - }); - - const posted = calls[0].body; - const userMsg = (posted.params as Record).messages[0] as Record< - string, - unknown - >; - assert.equal(typeof userMsg.content, "string"); - // Only text parts extracted; image_url part is dropped (not a "text" type) - assert.equal(userMsg.content, "Describe this:"); - } -); +test("#5166 user message with mixed parts (text + image_url) keeps all parts", async () => { + const calls = captureFetch(okResponse()); + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Describe this:" }, + { type: "image_url", image_url: { url: "https://example.com/img.png" } }, + ], + }, + ], + }, + }); + const userMsg = (calls[0].body.messages as Record[])[0]; + const parts = userMsg.content as Record[]; + assert.equal(parts.length, 2, "text + image both preserved"); + assert.equal(parts[0].text, "Describe this:"); + assert.equal(parts[1].type, "image_url"); +}); \ No newline at end of file diff --git a/tests/unit/command-code-vision.test.ts b/tests/unit/command-code-vision.test.ts index 80138b570b..3640139755 100644 --- a/tests/unit/command-code-vision.test.ts +++ b/tests/unit/command-code-vision.test.ts @@ -1,9 +1,13 @@ /** * Vision / multimodal support tests for the Command Code executor. * - * Verifies that vision-capable models (MiniMax M3, MiMo V2.5, Kimi K2, Qwen 3.x, GPT-5, Claude 3/4, Fable 5, Gemini 3.x, Stepfun, Fugu, etc.) - * receive image parts in Command Code CLI format, while text-only - * models strip images as before (no regression). + * Since #10265 the executor posts to the documented /provider/v1/chat/completions + * endpoint, which speaks the standard OpenAI chat.completions format. User image + * content (OpenAI `image_url` parts and Anthropic Messages-style source blocks) + * passes through unchanged — the endpoint natively understands both shapes, so + * there is no CLI-specific conversion (and no CLI-wire image stripping) left to + * verify. These tests pin that passthrough plus the #10809 wire-model + * normalization, which still applies to /provider/v1. */ import test from "node:test"; import assert from "node:assert/strict"; @@ -19,9 +23,8 @@ const core = await import("../../src/lib/db/core.ts"); const originalFetch = globalThis.fetch; -function commandCodeStream(lines: unknown[]) { - const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n"; - return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } }); +function okResponse() { + return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }); } test.after(() => { @@ -52,44 +55,28 @@ function captureFetch(response: Response) { } function userContent(calls: FetchCall[]): unknown { - return ( - (calls[0].body.params as Record).messages as Record[] - )[0].content; + return (calls[0].body.messages as Record[])[0].content; +} + +function wireModel(calls: FetchCall[]): string { + return calls[0].body.model as string; } // ── wire model normalization (#10809) ──────────────────────────────── -// -// Command Code's /alpha/generate endpoint serves most models under a -// vendor-prefixed wire id and defaults an unprefixed id to the `anthropic:` -// provider (403 "Model/provider not recognized: anthropic:"). A bare id -// reaches the executor when an operator sets a custom vision model in the -// Vision Bridge picker (e.g. `command-code/mimo-v2.5`). The executor must -// normalize to the documented vendor-prefixed wire form. - -function wireModel(calls: FetchCall[]): string { - return (calls[0].body.params as Record).model as string; -} test("#10809: command-code/mimo-v2.5 wire model is normalized to xiaomi/mimo-v2.5", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "command-code/mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, - body: { - model: "command-code/mimo-v2.5", - messages: [{ role: "user", content: "hi" }], - }, + body: { model: "command-code/mimo-v2.5", messages: [{ role: "user", content: "hi" }] }, }); assert.equal(wireModel(calls), "xiaomi/mimo-v2.5"); }); test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "cmd/mimo-v2.5", stream: false, @@ -100,9 +87,7 @@ test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => { }); test("#10809: already vendor-prefixed wire ids pass through unchanged", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "command-code/deepseek/deepseek-v4-pro", stream: false, @@ -115,13 +100,10 @@ test("#10809: already vendor-prefixed wire ids pass through unchanged", async () assert.equal(wireModel(calls), "deepseek/deepseek-v4-pro"); }); -// ── vision models: image parts preserved in CC CLI format ───────────── - -test("vision model minimax-m3 preserves image_url part as CC CLI {type:image}", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); +// ── image content passthrough (OpenAI /provider/v1 surface) ────────── +test("image_url parts pass through unchanged (text + image preserved)", async () => { + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "MiniMaxAI/MiniMax-M3", stream: false, @@ -132,451 +114,25 @@ test("vision model minimax-m3 preserves image_url part as CC CLI {type:image}", role: "user", content: [ { type: "text", text: "What's in this?" }, - { - type: "image_url", - image_url: { url: "data:image/png;base64,iVBORw0KGgo=" }, - }, + { type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo=" } }, ], }, ], }, }); - const content = userContent(calls); - assert.ok(Array.isArray(content), "vision model user content must be an array"); - const parts = content as Record[]; - assert.equal(parts.length, 2); - - // Text part preserved - assert.equal(parts[0].type, "text"); - assert.equal(parts[0].text, "What's in this?"); - - // Image part converted to CC CLI format - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "data:image/png;base64,iVBORw0KGgo="); + const content = userContent(calls) as Record[]; + assert.equal(content.length, 2); + assert.equal(content[0].type, "text"); + assert.equal(content[1].type, "image_url", "image_url part preserved as-is"); + assert.equal( + (content[1].image_url as { url: string }).url, + "data:image/png;base64,iVBORw0KGgo=" + ); }); -test("vision model minimax-m3 preserves image_url with HTTP URL", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "minimax-m3", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Describe" }, - { - type: "image_url", - image_url: { url: "https://example.com/photo.jpg" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/photo.jpg"); -}); - -test("vision model mimo-v2.5 preserves image parts", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "mimo-v2.5", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Analyze" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); -}); - -test("vision model mimo-v2.5-pro is text-only (no image parts)", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "mimo-v2.5-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Hi" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - // mimo-v2.5-pro is text-only — content must be flattened to a plain string - assert.equal(typeof content, "string"); - assert.equal(content, "Hi"); -}); - -test("vision model mimo-v2-omni preserves image parts", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "mimo-v2-omni", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Check" }, - { - type: "image_url", - image_url: { url: "data:image/jpeg;base64,/9j/4AAQ=" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "data:image/jpeg;base64,/9j/4AAQ="); -}); - -// ── non-vision models: images still stripped (no regression) ────────── - -test("text-only model deepseek-v4-pro strips image_url parts", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-pro", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Hello" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - // Non-vision model: content is a plain string, images stripped - assert.equal(typeof content, "string"); - assert.equal(content, "Hello"); -}); - -test("text-only model deepseek-v4-flash strips image_url parts (no regression)", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-flash", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Text only" }, - { - type: "image_url", - image_url: { url: "data:image/png;base64,AAA=" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.equal(typeof content, "string"); - assert.equal(content, "Text only"); -}); - -// ── edge cases ──────────────────────────────────────────────────────── - -test("vision model with only image content emits empty text fallback", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "minimax-m3", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { - type: "image_url", - image_url: { url: "data:image/png;base64,iVBOR=" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - // Single image part preserved — no empty text injected because - // the image itself keeps content non-empty. - assert.equal(parts.length, 1); - assert.equal(parts[0].type, "image"); - assert.equal(parts[0].image, "data:image/png;base64,iVBOR="); -}); - -test("vision model passes plain string content through unchanged", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "minimax-m3", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [{ role: "user", content: "Plain string message" }], - }, - }); - - const content = userContent(calls); - assert.equal(typeof content, "string"); - assert.equal(content, "Plain string message"); -}); - -test("vision model honors body.model rewrite for vision detection", async () => { - // #5166 scenario: body.model overwrites the execute model arg. - // Vision detection must use the rewritten model id. - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - // execute() gets a non-vision combo model, body.model rewrites to a vision model - await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - model: "MiniMaxAI/MiniMax-M3", - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Describe" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - // body.model = MiniMax-M3 (vision) → images preserved - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); -}); - -test("vision model with multiple image parts preserves all of them", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "minimax-m3", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Compare" }, - { - type: "image_url", - image_url: { url: "https://example.com/a.jpg" }, - }, - { - type: "image_url", - image_url: { url: "https://example.com/b.jpg" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 3); - assert.equal(parts[0].type, "text"); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/a.jpg"); - assert.equal(parts[2].type, "image"); - assert.equal(parts[2].image, "https://example.com/b.jpg"); -}); - -test("vision model with image_url as plain string (no object wrapper) still works", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "minimax-m3", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Look" }, - { - type: "image_url", - image_url: "https://example.com/img.png", - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/img.png"); -}); - -// ── CC vision models (Command Code docs registry) ────────────────── - -const VISION_CASES = [ - ["Kimi K2.6", "moonshotai/Kimi-K2.6"], - ["Kimi K2.7 Code", "moonshotai/Kimi-K2.7-Code"], - ["Kimi K2.5", "moonshotai/Kimi-K2.5"], - ["Qwen 3.6 Plus", "Qwen/Qwen3.6-Plus"], - ["Qwen 3.7 Plus", "Qwen/Qwen3.7-Plus"], - ["Step 3.7 Flash", "stepfun/Step-3.7-Flash"], - ["GPT-5.5", "gpt-5.5"], - ["GPT-5.4", "gpt-5.4"], - ["GPT-5.3 Codex", "gpt-5.3-codex"], - ["GPT-5.4 Mini", "gpt-5.4-mini"], - ["Claude Fable 5", "claude-fable-5"], - ["Sakana Fugu Ultra", "sakana/fugu-ultra"], - ["Claude Opus 4.7 (isVisionModelId)", "claude-opus-4-7"], - ["Claude Sonnet 4.6 (isVisionModelId)", "claude-sonnet-4-6"], - ["Gemini 3.5 Flash (isVisionModelId)", "google/gemini-3.5-flash"], -]; - -for (const [name, model] of VISION_CASES) { - test(`vision model ${name} preserves image parts`, async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - await getExecutor("command-code").execute({ - model, - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Check" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - const content = userContent(calls); - assert.ok(Array.isArray(content), `${name} user content must be an array`); - const parts = content; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - }); -} - -// ── Anthropic-shaped image blocks (Zoo Code / Claude-Code-compatible clients) ── - -test("vision model mimo-v2.5 preserves Anthropic source.base64 image block", async () => { - // Zoo Code sends Messages-API-shaped content blocks to the OpenAI - // /v1/chat/completions surface: { type:"image", source:{ base64 } }. - // The vision-bridge guardrail skips vision-capable models (cmd/xiaomi/mimo-v2.5 - // resolves supportsVision=true via the mimo-v2.5 leaf spec), so the raw block - // must survive to the executor and be converted to CC CLI { type:"image" }. - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - +test("Anthropic Messages-style source image blocks pass through unchanged", async () => { + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "xiaomi/mimo-v2.5", stream: false, @@ -601,24 +157,15 @@ test("vision model mimo-v2.5 preserves Anthropic source.base64 image block", asy }, }); - const content = userContent(calls); - assert.ok(Array.isArray(content), "user content must be an array"); - const parts = content as Record[]; - assert.equal(parts.length, 2, "text + image parts preserved"); - assert.equal(parts[0].type, "text"); - assert.equal(parts[1].type, "image"); - assert.equal( - parts[1].image, - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", - "base64 payload is rebuilt into a CC CLI data URL" - ); + const content = userContent(calls) as Record[]; + assert.equal(content.length, 2, "text + image parts preserved"); + assert.equal(content[1].type, "image"); + assert.equal((content[1].source as { type: string }).type, "base64"); + assert.equal((content[1].source as { data: string }).data, "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="); }); -test("vision model preserves Anthropic source.url image block", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - +test("Anthropic source.url image block passes through unchanged", async () => { + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ model: "xiaomi/mimo-v2.5", stream: false, @@ -629,31 +176,23 @@ test("vision model preserves Anthropic source.url image block", async () => { role: "user", content: [ { type: "text", text: "Look" }, - { - type: "image", - source: { type: "url", url: "https://example.com/img.png" }, - }, + { type: "image", source: { type: "url", url: "https://example.com/img.png" } }, ], }, ], }, }); - const content = userContent(calls); - assert.ok(Array.isArray(content)); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/img.png"); + const content = userContent(calls) as Record[]; + assert.equal(content.length, 2); + assert.equal(content[1].type, "image"); + assert.deepEqual(content[1].source, { type: "url", url: "https://example.com/img.png" }); }); -test("text-only model deepseek-v4-flash strips Anthropic source.base64 image block", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - +test("multiple image parts are all preserved", async () => { + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ - model: "deepseek/deepseek-v4-flash", + model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { @@ -661,44 +200,41 @@ test("text-only model deepseek-v4-flash strips Anthropic source.base64 image blo { role: "user", content: [ - { type: "text", text: "Text only" }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", - }, - }, + { type: "text", text: "Compare" }, + { type: "image_url", image_url: { url: "https://example.com/a.jpg" } }, + { type: "image_url", image_url: { url: "https://example.com/b.jpg" } }, ], }, ], }, }); + const content = userContent(calls) as Record[]; + assert.equal(content.length, 3); + assert.equal(content[1].type, "image_url"); + assert.equal(content[2].type, "image_url"); +}); + +test("plain string content passes through unchanged", async () => { + const calls = captureFetch(okResponse()); + await getExecutor("command-code").execute({ + model: "minimax-m3", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Plain string message" }] }, + }); + const content = userContent(calls); - // Text-only model: content flattened to plain string, image stripped. assert.equal(typeof content, "string"); - assert.equal(content, "Text only"); + assert.equal(content, "Plain string message"); }); -// ── conservative vision family lock (gpt-5.4-mini / gpt-5.3-codex) ───── -// -// These two ids stay INSIDE the `/gpt-5/` vision family: both accept image -// input on the OpenAI API, and there is no verified Command Code backend data -// marking them text-only. These tests pin that conservative executor behavior -// so a future "narrow the regex" change cannot silently strip images from models -// that can see them (the #4071 regression class). The #10703 Vision Bridge -// candidate-list fix lives in the shared capability resolution -// (KNOWN_TEXT_ONLY_DESPITE_SYNC), NOT in the executor's wire transform. - -test("gpt-5.4-mini keeps image parts (conservative vision family lock)", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - +test("text-only model still forwards image parts (passthrough, no CLI stripping)", async () => { + // The /provider/v1 OpenAI surface accepts image content for any model id; the + // executor forwards content untouched, so there is no text-only stripping. + const calls = captureFetch(okResponse()); await getExecutor("command-code").execute({ - model: "gpt-5.4-mini", + model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, body: { @@ -706,54 +242,15 @@ test("gpt-5.4-mini keeps image parts (conservative vision family lock)", async ( { role: "user", content: [ - { type: "text", text: "What's in this?" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, + { type: "text", text: "Hello" }, + { type: "image_url", image_url: { url: "https://example.com/img.png" } }, ], }, ], }, }); - const content = userContent(calls); - assert.ok(Array.isArray(content), "gpt-5.4-mini must be treated as vision-capable"); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/img.png"); -}); - -test("gpt-5.3-codex keeps image parts (conservative vision family lock)", async () => { - const calls = captureFetch( - commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) - ); - - await getExecutor("command-code").execute({ - model: "gpt-5.3-codex", - stream: false, - credentials: { apiKey: "cc_test_key" }, - body: { - messages: [ - { - role: "user", - content: [ - { type: "text", text: "Describe" }, - { - type: "image_url", - image_url: { url: "https://example.com/img.png" }, - }, - ], - }, - ], - }, - }); - - const content = userContent(calls); - assert.ok(Array.isArray(content), "gpt-5.3-codex must be treated as vision-capable"); - const parts = content as Record[]; - assert.equal(parts.length, 2); - assert.equal(parts[1].type, "image"); - assert.equal(parts[1].image, "https://example.com/img.png"); -}); + const content = userContent(calls) as Record[]; + assert.equal(content.length, 2, "content array forwarded unchanged"); + assert.equal(content[1].type, "image_url"); +}); \ No newline at end of file diff --git a/tests/unit/executor-command-code.test.ts b/tests/unit/executor-command-code.test.ts index 06b8a51afd..56ee16b727 100644 --- a/tests/unit/executor-command-code.test.ts +++ b/tests/unit/executor-command-code.test.ts @@ -14,11 +14,15 @@ describe("CommandCodeExecutor", () => { assert.ok(executor); }); - it("buildUrl returns a string", () => { + it("buildUrl targets the documented /provider/v1/chat/completions endpoint (#10265)", () => { const executor = new mod.CommandCodeExecutor(); const url = executor.buildUrl(); assert.ok(typeof url === "string"); - assert.ok(url.includes("generate") && url.includes("commandcode")); + assert.ok( + url.includes("/provider/v1/chat/completions"), + `expected the documented provider API endpoint, got: ${url}` + ); + assert.ok(url.includes("commandcode")); }); it("execute throws when no API key", async () => { @@ -57,7 +61,7 @@ describe("CommandCodeExecutor", () => { } }); - it("assistant tool-call conversion always emits a valid required arguments field (#regression input[N] missing required field arguments)", async () => { + it("posts a flat OpenAI chat.completions body (no CLI envelope) to /provider/v1/chat/completions (#10265)", async () => { const calls: Array<{ url: string; init: RequestInit; body: unknown }> = []; const originalFetch = globalThis.fetch; globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { @@ -70,182 +74,8 @@ describe("CommandCodeExecutor", () => { }) as typeof fetch; const executor = new mod.CommandCodeExecutor(); - const pairedId = "call_paired"; - const body = { - messages: [ - { role: "user", content: "hi" }, - { - role: "assistant", - content: "", - tool_calls: [ - // Missing arguments entirely -> must still get a valid arguments field - { id: "call_missing", type: "function", function: { name: "lookup" } }, - // Empty string arguments -> "{}" - { - id: "call_empty", - type: "function", - function: { name: "lookup", arguments: "" }, - }, - // Valid object arguments -> round-trips as JSON string - { - id: pairedId, - type: "function", - function: { name: "lookup", arguments: { q: "docs" } }, - }, - // Valid string arguments -> preserved as-is - { - id: "call_string", - type: "function", - function: { name: "lookup", arguments: '{"q":"string"}' }, - }, - // Invalid JSON string arguments -> defaults to "{}" - { - id: "call_invalid", - type: "function", - function: { name: "lookup", arguments: "{invalid-json" }, - }, - // Tool call without name -> defaults tool-result toolName to "unknown" - { - id: "call_unnamed", - type: "function", - function: { arguments: { q: "unnamed" } }, - }, - ], - }, - { role: "tool", tool_call_id: "call_missing", content: "r1" }, - { role: "tool", tool_call_id: "call_empty", content: "r2" }, - { role: "tool", tool_call_id: pairedId, content: "r3" }, - { role: "tool", tool_call_id: "call_string", content: "r4" }, - { role: "tool", tool_call_id: "call_invalid", content: "r5" }, - { role: "tool", tool_call_id: "call_unnamed", content: "r6" }, - ], - }; - - try { - await executor.execute({ - model: "test", - body, - stream: false, - credentials: { apiKey: "fake-key" }, - signal: null, - }); - } finally { - globalThis.fetch = originalFetch; - } - - assert.equal(calls.length, 1, "exactly one upstream call"); - const sentBody = calls[0].body as { - params: { messages: Array<{ role: string; content: unknown }> }; - }; - const assistant = sentBody.params.messages.find((m) => m.role === "assistant"); - assert.ok(assistant, "assistant turn present"); - const parts = assistant.content as Array>; - const toolCalls = parts.filter((p) => p.type === "tool-call"); - assert.equal(toolCalls.length, 6, "all six paired tool calls converted"); - - for (const call of toolCalls) { - assert.equal( - typeof call.arguments, - "string", - `tool-call ${String(call.toolCallId)} must carry a string arguments field` - ); - const parsed = JSON.parse(call.arguments as string); - assert.equal(typeof parsed, "object"); - assert.ok(!Array.isArray(parsed), "arguments must parse to a JSON object"); - } - - const byId = new Map(toolCalls.map((c) => [String(c.toolCallId), c])); - assert.equal(byId.get("call_missing").arguments, "{}", "missing arguments -> empty object"); - assert.equal(byId.get("call_empty").arguments, "{}", "empty string arguments -> empty object"); - assert.equal( - byId.get(pairedId).arguments, - '{"q":"docs"}', - "object arguments round-trip as JSON string" - ); - assert.equal( - byId.get("call_string").arguments, - '{"q":"string"}', - "valid string arguments preserved as-is" - ); - assert.equal( - byId.get("call_invalid").arguments, - "{}", - "invalid JSON string arguments -> empty object" - ); - - const toolMsgs = sentBody.params.messages.filter((m) => m.role === "tool"); - assert.equal(toolMsgs.length, 6, "all 6 tool result messages present"); - const resultByName = new Map( - toolMsgs.map((m) => { - const p = (m.content as Array>)[0]; - return [String(p.toolCallId), String(p.toolName)]; - }) - ); - assert.equal(resultByName.get("call_missing"), "lookup"); - assert.equal( - resultByName.get("call_unnamed"), - "unknown", - "unnamed call falls back to 'unknown'" - ); - - // /alpha/generate also requires `arguments` on tool-result parts; a - // missing one is rejected with `input[N] missing required field 'arguments'` - // (the index landing on the tool message). Echo the paired call's - // normalized arguments. - const resultById = new Map( - toolMsgs.map((m) => { - const p = (m.content as Array>)[0]; - return [String(p.toolCallId), p]; - }) - ); - assert.equal(resultById.size, 6, "each tool result maps to its call id"); - for (const p of resultById.values()) { - assert.equal( - typeof p.arguments, - "string", - `tool-result ${String(p.toolCallId)} must carry a string arguments field` - ); - const parsed = JSON.parse(p.arguments as string); - assert.equal(typeof parsed, "object"); - assert.ok(!Array.isArray(parsed), "tool-result arguments must parse to a JSON object"); - } - assert.equal( - resultById.get("call_missing").arguments, - "{}", - "tool-result echoes paired call's missing arguments as empty object" - ); - assert.equal( - resultById.get(pairedId).arguments, - '{"q":"docs"}', - "tool-result echoes paired call's object arguments as JSON string" - ); - assert.equal( - resultById.get("call_string").arguments, - '{"q":"string"}', - "tool-result echoes paired call's valid string arguments as-is" - ); - assert.equal( - resultById.get("call_empty").arguments, - "{}", - "tool-result echoes paired call's empty arguments as empty object" - ); - assert.equal( - resultById.get("call_invalid").arguments, - "{}", - "tool-result echoes paired call's invalid JSON arguments as empty object" - ); - }); - - it("COMMAND_CODE_VERSION default constant is 1.15.1", () => { - assert.equal(mod.COMMAND_CODE_VERSION, "1.15.1"); - }); - - it("renames tool names colliding with upstream built-ins on the wire and un-renames on the response (#regression input[N] missing required field arguments from a tool_search result)", async () => { - // Upstream /alpha/generate normalizes tool-call/result parts against its - // OWN built-in registry for matching names; `tool_search` collides and its - // result is rejected with `input[N] missing required field 'arguments'`. - // Verified live: renaming the pair to a non-colliding name passes. const body = { + model: "gpt-5.4", messages: [ { role: "user", content: "hi" }, { @@ -253,29 +83,19 @@ describe("CommandCodeExecutor", () => { content: "", tool_calls: [ { - id: "call_00_AAAAAAAAAAAAAAAAA", + id: "call_1", type: "function", - function: { name: "tool_search", arguments: '{"query":"x"}' }, - }, - { - id: "call_01_BBBBBBBBBBBBBBBBB", - type: "function", - function: { name: "lookup", arguments: '{"q":"1"}' }, + function: { name: "lookup", arguments: '{"q":"docs"}' }, }, + // Missing arguments entirely stays missing — passthrough, no CLI + // envelope injection of a synthetic `arguments` field. + { id: "call_2", type: "function", function: { name: "search" } }, ], }, - { role: "tool", tool_call_id: "call_00_AAAAAAAAAAAAAAAAA", content: "r1" }, - { role: "tool", tool_call_id: "call_01_BBBBBBBBBBBBBBBBB", content: "r2" }, + { role: "tool", tool_call_id: "call_1", content: "r1" }, + { role: "tool", tool_call_id: "call_2", content: "r2" }, ], tools: [ - { - type: "function", - function: { - name: "tool_search", - description: "search tools", - parameters: { type: "object", properties: { query: { type: "string" } } }, - }, - }, { type: "function", function: { @@ -287,31 +107,9 @@ describe("CommandCodeExecutor", () => { ], }; - const calls: Array<{ url: string; init: RequestInit; body: unknown }> = []; - const originalFetch = globalThis.fetch; - // execute() makes a single upstream fetch; capture the wire request and - // return a stream with a tool-call event using the renamed wire name. - globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { - calls.push({ - url: String(url), - init: init || {}, - body: JSON.parse(String((init as RequestInit | undefined)?.body)), - }); - const streamBody = - 'data: {"type":"tool-call","toolCallId":"c1","toolName":"omniroute_tool_search","input":{"query":"x"}}\n\n' + - 'data: {"type":"finish","finishReason":"tool_use"}\n\n' + - "data: [DONE]\n\n"; - return new Response(streamBody, { - status: 200, - headers: { "Content-Type": "text/event-stream" }, - }); - }) as typeof fetch; - - const executor = new mod.CommandCodeExecutor(); - let result: { response: Response } | null = null; try { - result = await executor.execute({ - model: "test", + await executor.execute({ + model: "gpt-5.4", body, stream: false, credentials: { apiKey: "fake-key" }, @@ -321,46 +119,85 @@ describe("CommandCodeExecutor", () => { globalThis.fetch = originalFetch; } - const sentBody = calls[0].body as { - params: { - messages: Array<{ role: string; content: unknown }>; - tools: Array<{ name: string }>; - }; - }; - const toolDefNames = sentBody.params.tools.map((t) => t.name); + assert.equal(calls.length, 1, "exactly one upstream call"); assert.ok( - toolDefNames.includes("omniroute_tool_search"), - "colliding tool def renamed on the wire" + calls[0].url.includes("/provider/v1/chat/completions"), + `expected documented provider endpoint, got: ${calls[0].url}` ); - assert.ok(toolDefNames.includes("lookup"), "non-colliding tool def untouched"); - - const assistant = sentBody.params.messages.find((m) => m.role === "assistant"); - const toolCallParts = (assistant?.content as Array>).filter( - (p) => p.type === "tool-call" + const sent = calls[0].body as Record; + // No CLI envelope. + assert.equal(sent.config, undefined, "CLI envelope `config` must not be sent"); + assert.equal(sent.params, undefined, "CLI envelope `params` wrapper must not be sent"); + assert.equal(sent.model, "gpt-5.4", "flat OpenAI model at top level"); + assert.equal((sent.messages as Array<{ role: string }>)[0].role, "user"); + // Assistant tool_calls pass through unchanged (no CLI tool-call/tool-result parts). + const assistant = (sent.messages as Array>).find( + (m) => m.role === "assistant" ); - const toolSearchCall = toolCallParts.find((p) => p.toolName === "omniroute_tool_search"); - assert.ok(toolSearchCall, "assistant tool-call part renamed on the wire"); - const lookupCall = toolCallParts.find((p) => p.toolName === "lookup"); - assert.ok(lookupCall, "non-colliding tool-call part untouched"); - - const toolMsgs = sentBody.params.messages.filter((m) => m.role === "tool"); - const toolSearchResult = toolMsgs.find( - (m) => (m.content as Array>)[0]?.toolName === "omniroute_tool_search" + assert.ok(assistant, "assistant turn present"); + const toolCalls = assistant?.tool_calls as Array<{ + id: string; + function: { name: string; arguments?: string }; + }>; + assert.equal(toolCalls.length, 2, "both tool calls pass through untouched"); + assert.equal(toolCalls[0].function.name, "lookup"); + assert.equal(toolCalls[0].function.arguments, '{"q":"docs"}'); + assert.equal(toolCalls[1].function.arguments, undefined, "missing arguments stays missing (no injection)"); + // Tool role message (OpenAI flat) preserved. + const toolMsg = (sent.messages as Array>).find( + (m) => m.role === "tool" ); - assert.ok(toolSearchResult, "tool-result part renamed on the wire"); - - // Response path: upstream emits the renamed wire name; the client must get - // its original name back. - assert.ok(result, "execute returned a response"); - const json = (await result.response.json()) as { - choices: Array<{ message: { tool_calls?: Array<{ function: { name: string } }> } }>; - }; - const toolCalls = json.choices[0].message.tool_calls ?? []; - assert.equal(toolCalls.length, 1, "one tool call translated"); + assert.equal(toolMsg?.tool_call_id, "call_1"); assert.equal( - toolCalls[0].function.name, - "tool_search", - "renamed wire name un-renamed for the client" + (sent.tools as Array<{ function: { name: string } }>)[0].function.name, + "lookup", + "tool definitions pass through in OpenAI shape (no rename)" ); }); -}); + + it("passes through the upstream OpenAI response and drops CLI-impersonation headers (#10265)", async () => { + const calls: Array<{ url: string; init: RequestInit }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + calls.push({ url: String(url), init: init || {} }); + const chunk = + 'data: {"id":"c1","object":"chat.completion.chunk","model":"gpt-5.4",' + + '"choices":[{"index":0,"delta":{"content":"hi"}}]}\n\n' + + 'data: {"id":"c1","object":"chat.completion.chunk","model":"gpt-5.4",' + + '"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":' + + '{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}\n\n' + + "data: [DONE]\n\n"; + return new Response(chunk, { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }); + }) as typeof fetch; + + const executor = new mod.CommandCodeExecutor(); + let result: { response: Response; headers: Record } | null = null; + try { + result = await executor.execute({ + model: "gpt-5.4", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: { apiKey: "fake-key" }, + signal: null, + }); + } finally { + globalThis.fetch = originalFetch; + } + + assert.ok(result, "execute returned a result"); + const headers = result.headers; + assert.equal(headers["x-command-code-version"], undefined, "CLI-impersonation header dropped"); + assert.equal(headers["x-cli-environment"], undefined, "CLI-impersonation header dropped"); + assert.equal(headers.Authorization, "Bearer fake-key"); + + // The upstream OpenAI SSE passes through untouched (no CLI re-parsing). + const text = await result.response.text(); + assert.ok(text.includes("chat.completion.chunk"), "OpenAI-format SSE passed through"); + assert.ok(text.includes('"content":"hi"'), "delta content preserved"); + assert.ok(text.includes("[DONE]"), "stream terminator preserved"); + assert.ok(text.includes('"prompt_tokens":2'), "OpenAI usage block passed through"); + }); +}); \ No newline at end of file diff --git a/tests/unit/provider-models-target-format-scoping.test.ts b/tests/unit/provider-models-target-format-scoping.test.ts index b899eb960a..106fd7a020 100644 --- a/tests/unit/provider-models-target-format-scoping.test.ts +++ b/tests/unit/provider-models-target-format-scoping.test.ts @@ -8,12 +8,12 @@ import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/ta // ghe-copilot catalog. getModelTargetFormat falls back to getGlobalModel() when // the provider's own catalog lacks the model id, importing the DECLARING // provider's endpoint semantics into every other provider serving the same id. -// command-code's chat-shaped /alpha/generate executor then received a +// command-code's chat-shaped executor then received a // Responses-format body (input, not messages) and shipped `messages: []` // upstream — upstream rejected with "Invalid prompt: messages must not be empty" // (502). Model-level targetFormat is provider-scoped: it must not leak. test("model-level targetFormat does not leak across provider catalogs", () => { - // command-code serves gpt-5.6-luna over its chat-shaped /alpha/generate endpoint + // command-code serves gpt-5.6-luna over its chat-shaped provider endpoint assert.equal(getModelTargetFormat("cmd", "gpt-5.6-luna"), null); // raw provider id form behaves identically (alias resolution) assert.equal(getModelTargetFormat("command-code", "gpt-5.6-luna"), null); diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index ce172cd05f..cebf699d17 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -16,7 +16,6 @@ const { __setTlsFetchOverrideForTesting: __setPplxTlsFetchOverride } = const { __setTlsFetchOverrideForTesting: __setGrokTlsFetchOverride } = await import("../../open-sse/services/grokTlsClient.ts"); -const { COMMAND_CODE_VERSION } = await import("../../open-sse/executors/commandCode.ts"); const originalFetch = globalThis.fetch; @@ -216,11 +215,11 @@ test("specialty provider validators cover Deepgram, AssemblyAI, ElevenLabs and I test("validateCommandCodeProvider ignores caller baseUrl and chatPath overrides", async () => { globalThis.fetch = async (url, init = {}) => { - assert.equal(String(url), "https://api.commandcode.ai/alpha/generate"); + assert.equal(String(url), "https://api.commandcode.ai/provider/v1/chat/completions"); const headers = init.headers as Record; assert.equal(headers.Authorization, "Bearer cc-key"); const body = JSON.parse(String(init.body)); - assert.equal(body.params.model, "command-code-validation-model"); + assert.equal(body.model, "command-code-validation-model"); return new Response(JSON.stringify({ ok: true }), { status: 200 }); }; @@ -239,7 +238,7 @@ test("validateCommandCodeProvider ignores caller baseUrl and chatPath overrides" test("validateCommandCodeProvider defaults probe model to DeepSeek flash", async () => { globalThis.fetch = async (_url, init = {}) => { const body = JSON.parse(String(init.body)); - assert.equal(body.params.model, "deepseek/deepseek-v4-flash"); + assert.equal(body.model, "deepseek/deepseek-v4-flash"); return new Response("", { status: 400 }); }; @@ -2285,7 +2284,7 @@ test("specialty validator rejects invalid Runway credentials", async () => { assert.equal(runway.error, "Invalid API key"); }); -test("validateCommandCodeProvider sends Command Code probe URL, headers, and wrapper body", async () => { +test("validateCommandCodeProvider sends Command Code probe URL, headers, and flat OpenAI body", async () => { const calls: Array<{ url: string; method?: string; @@ -2309,22 +2308,21 @@ test("validateCommandCodeProvider sends Command Code probe URL, headers, and wra assert.deepEqual(result, { valid: true, error: null }); assert.equal(calls.length, 1); - assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate"); + // Probe targets the documented /provider/v1/chat/completions endpoint, not + // the CLI-only /alpha/generate (#10265). + assert.equal(calls[0].url, "https://api.commandcode.ai/provider/v1/chat/completions"); assert.equal(calls[0].method, "POST"); assert.equal(calls[0].headers.Authorization, "Bearer cc_test_key"); assert.equal(calls[0].headers["Content-Type"], "application/json"); - assert.equal(calls[0].headers["x-command-code-version"], COMMAND_CODE_VERSION); - assert.equal(calls[0].headers["x-cli-environment"], "external"); - assert.equal(calls[0].headers["x-project-slug"], "pi-cc"); - assert.equal(calls[0].headers["x-taste-learning"], "false"); - assert.equal(calls[0].headers["x-co-flag"], "false"); - assert.equal(typeof calls[0].headers["x-session-id"], "string"); - assert.equal(calls[0].body.config.environment, "external"); - assert.equal(calls[0].body.permissionMode, "standard"); - assert.equal(calls[0].body.skills, ""); - assert.equal(calls[0].body.params.model, "gpt-5.4-mini"); - assert.equal(calls[0].body.params.stream, true); - assert.equal(calls[0].body.params.max_tokens, 1); + // No CLI-impersonation headers. + assert.equal(calls[0].headers["x-command-code-version"], undefined); + assert.equal(calls[0].headers["x-cli-environment"], undefined); + assert.equal(calls[0].headers["x-project-slug"], undefined); + // Flat OpenAI chat.completions body (no CLI wrapper). + assert.equal(calls[0].body.params, undefined, "CLI envelope params wrapper must not be sent"); + assert.equal(calls[0].body.model, "gpt-5.4-mini"); + assert.equal(calls[0].body.stream, true); + assert.equal(calls[0].body.max_tokens, 1); }); for (const status of [400, 422, 429]) { diff --git a/tests/unit/responses-handler.test.ts b/tests/unit/responses-handler.test.ts index 7afbe12d31..3b4d4b7cde 100644 --- a/tests/unit/responses-handler.test.ts +++ b/tests/unit/responses-handler.test.ts @@ -10,7 +10,6 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const { handleResponsesCore } = await import("../../open-sse/handlers/responsesHandler.ts"); -const { COMMAND_CODE_VERSION } = await import("../../open-sse/executors/commandCode.ts"); const originalFetch = globalThis.fetch; @@ -354,26 +353,31 @@ test("handleResponsesCore transforms Command Code executor SSE through Responses input: "hello command code", }, responseFactory() { + // /provider/v1/chat/completions returns standard OpenAI SSE (#10265). + const chunk = (delta: Record) => + `data: ${JSON.stringify({ + id: "c1", + object: "chat.completion.chunk", + model: "gpt-5.4-mini", + choices: [{ index: 0, delta }], + })}\n\n`; return new Response( [ - `data: ${JSON.stringify({ type: "text-delta", text: "command" })}`, - "", - `data: ${JSON.stringify({ type: "reasoning-delta", text: "thinking" })}`, - "", - `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}`, - "", - ].join("\n"), - { status: 200, headers: { "Content-Type": "application/x-ndjson" } } + chunk({ role: "assistant" }), + chunk({ content: "command" }), + chunk({}), + ].join("") + "data: [DONE]\n\n", + { status: 200, headers: { "Content-Type": "text/event-stream" } } ); }, }); assert.equal(result.success, true); - assert.equal(call.url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(call.url, "https://api.commandcode.ai/provider/v1/chat/completions"); assert.equal(call.headers.Authorization, "Bearer cc_test_key"); - assert.equal(call.headers["x-command-code-version"], COMMAND_CODE_VERSION); - assert.equal(call.body.params.model, "gpt-5.4-mini"); - assert.equal(call.body.params.stream, true); + assert.equal(call.headers["x-command-code-version"], undefined); + assert.equal(call.body.model, "gpt-5.4-mini"); + assert.equal(call.body.stream, true); const sse = await result.response.text(); assert.match(sse, /event: response\.created/);