From fc620514a91c1cb64eaea0ed3b253b094b55625c Mon Sep 17 00:00:00 2001 From: Dohyun Jung Date: Wed, 13 May 2026 07:57:35 +0900 Subject: [PATCH] feat(providers): add Command Code provider (#2199) Integrated into release/v3.8.0 after syncing the contributor branch, removing unrelated workflow/package-lock changes, and validating Command Code provider, auth, validation, and Responses coverage locally. --- open-sse/config/providerRegistry.ts | 143 +++++ open-sse/executors/commandCode.ts | 545 ++++++++++++++++++ open-sse/executors/index.ts | 4 + public/providers/command-code.svg | 42 ++ .../dashboard/providers/[id]/page.tsx | 538 ++++++++++++++++- .../command-code/auth/apply/route.ts | 101 ++++ .../command-code/auth/callback/route.ts | 67 +++ .../api/providers/command-code/auth/shared.ts | 120 ++++ .../command-code/auth/start/route.ts | 28 + .../command-code/auth/status/route.ts | 38 ++ src/lib/db/commandCodeAuth.ts | 213 +++++++ .../055_command_code_auth_sessions.sql | 19 + src/lib/providers/validation.ts | 51 +- src/shared/components/ProviderIcon.tsx | 1 + src/shared/constants/providers.ts | 12 + tests/unit/command-code-auth-assist.test.ts | 231 ++++++++ tests/unit/command-code-executor.test.ts | 249 ++++++++ .../provider-validation-specialty.test.ts | 96 ++- tests/unit/responses-handler.test.ts | 39 ++ 19 files changed, 2514 insertions(+), 23 deletions(-) create mode 100644 open-sse/executors/commandCode.ts create mode 100644 public/providers/command-code.svg create mode 100644 src/app/api/providers/command-code/auth/apply/route.ts create mode 100644 src/app/api/providers/command-code/auth/callback/route.ts create mode 100644 src/app/api/providers/command-code/auth/shared.ts create mode 100644 src/app/api/providers/command-code/auth/start/route.ts create mode 100644 src/app/api/providers/command-code/auth/status/route.ts create mode 100644 src/lib/db/commandCodeAuth.ts create mode 100644 src/lib/db/migrations/055_command_code_auth_sessions.sql create mode 100644 tests/unit/command-code-auth-assist.test.ts create mode 100644 tests/unit/command-code-executor.test.ts diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 8f01d8b584..baa19cd1ba 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -156,6 +156,135 @@ const KIMI_CODING_SHARED = { const buildModels = (ids: readonly string[]): RegistryModel[] => ids.map((id) => ({ id, name: id })); +const COMMAND_CODE_MODELS: RegistryModel[] = [ + { + id: "claude-opus-4-7", + name: "Claude Opus 4.7 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 32000, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 32000, + }, + { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 16384, + }, + { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 8192, + }, + { + id: "gpt-5.5", + name: "GPT-5.5 (CC)", + supportsReasoning: true, + contextLength: 256000, + maxOutputTokens: 128000, + }, + { + id: "gpt-5.4", + name: "GPT-5.4 (CC)", + supportsReasoning: true, + contextLength: 256000, + maxOutputTokens: 128000, + }, + { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex (CC)", + supportsReasoning: true, + contextLength: 256000, + maxOutputTokens: 128000, + }, + { + id: "gpt-5.4-mini", + name: "GPT-5.4 Mini (CC)", + supportsReasoning: false, + contextLength: 256000, + maxOutputTokens: 128000, + }, + { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek V4 Pro (CC)", + supportsReasoning: true, + contextLength: 1000000, + maxOutputTokens: 384000, + }, + { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek V4 Flash (CC)", + supportsReasoning: true, + contextLength: 1000000, + maxOutputTokens: 384000, + }, + { + id: "moonshotai/Kimi-K2.6", + name: "Kimi K2.6 (CC)", + supportsReasoning: true, + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "moonshotai/Kimi-K2.5", + name: "Kimi K2.5 (CC)", + supportsReasoning: true, + contextLength: 262144, + maxOutputTokens: 131072, + }, + { + id: "zai-org/GLM-5.1", + name: "GLM-5.1 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 131072, + }, + { + id: "zai-org/GLM-5", + name: "GLM-5 (CC)", + supportsReasoning: true, + contextLength: 200000, + maxOutputTokens: 131072, + }, + { + id: "MiniMaxAI/MiniMax-M2.7", + name: "MiniMax M2.7 (CC)", + supportsReasoning: true, + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax M2.5 (CC)", + supportsReasoning: true, + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3.6-Max-Preview", + name: "Qwen 3.6 Max (CC)", + supportsReasoning: true, + contextLength: 1000000, + maxOutputTokens: 131072, + }, + { + id: "Qwen/Qwen3.6-Plus", + name: "Qwen 3.6 Plus (CC)", + supportsReasoning: true, + contextLength: 1000000, + maxOutputTokens: 131072, + }, +]; + const GPT_5_5_CONTEXT_LENGTH = 1050000; const GPT_5_5_CODEX_CAPABILITIES = { targetFormat: "openai-responses", @@ -991,6 +1120,20 @@ export const REGISTRY: Record = { passthroughModels: true, }, + "command-code": { + id: "command-code", + alias: "cmd", + format: "openai", + executor: "command-code", + baseUrl: "https://api.commandcode.ai", + chatPath: "/alpha/generate", + authType: "apikey", + authHeader: "Authorization", + authPrefix: "Bearer ", + defaultContextLength: 200000, + models: COMMAND_CODE_MODELS, + }, + openrouter: { id: "openrouter", alias: "openrouter", diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts new file mode 100644 index 0000000000..22b14f3a5c --- /dev/null +++ b/open-sse/executors/commandCode.ts @@ -0,0 +1,545 @@ +import { randomUUID } from "node:crypto"; + +import { REGISTRY } from "../config/providerRegistry.ts"; +import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./base.ts"; + +type JsonRecord = Record; + +const COMMAND_CODE_VERSION = "0.24.1"; +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 { + // Tool argument fragments may be incomplete in streamed deltas. + } + } + return {}; +} + +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"); +} + +function convertTools(tools: unknown): unknown[] { + return asRecordArray(tools).map((tool) => { + const fn = isRecord(tool.function) ? tool.function : tool; + return { + type: "function", + name: stringValue(fn.name) || "", + description: stringValue(fn.description) || "", + input_schema: isRecord(fn.parameters) ? fn.parameters : {}, + }; + }); +} + +function completeToolCallIds(messages: JsonRecord[]): Set { + const callIds = new Set(); + const resultIds = new Set(); + + 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); + } + } else if (message.role === "tool") { + const id = stringValue(message.tool_call_id); + if (id) resultIds.add(id); + } + } + + return new Set([...callIds].filter((id) => resultIds.has(id))); +} + +function convertMessages(messages: unknown): { system: string; messages: unknown[] } { + const source = asRecordArray(messages); + const pairedToolCallIds = completeToolCallIds(source); + const out: unknown[] = []; + const system: string[] = []; + + 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: message.content ?? "" }); + 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 : {}; + parts.push({ + type: "tool-call", + toolCallId: id, + toolName: stringValue(fn.name) || "", + input: recordOrEmpty(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; + out.push({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId, + toolName: stringValue(message.name) || "", + output: { type: "text", value: normalizeContentText(message.content) }, + }, + ], + }); + } + } + + return { system: system.join("\n\n"), messages: out }; +} + +function clampMaxTokens(value: unknown): number { + const numeric = numberValue(value) ?? MAX_COMMAND_CODE_TOKENS; + return Math.max(1, Math.min(Math.floor(numeric), MAX_COMMAND_CODE_TOKENS)); +} + +function buildCommandCodeBody(model: string, body: unknown): JsonRecord { + const input = isRecord(body) ? body : {}; + const converted = convertMessages(input.messages); + const explicitSystem = typeof input.system === "string" ? input.system : ""; + const system = [converted.system, explicitSystem].filter(Boolean).join("\n\n"); + + return { + config: { + workingDir: "/workspace", + date: new Date().toISOString().slice(0, 10), + environment: "omniroute", + structure: [], + isGitRepo: false, + currentBranch: "", + mainBranch: "", + gitStatus: "", + recentCommits: [], + }, + memory: "", + taste: "", + skills: null, + permissionMode: "standard", + params: { + model, + messages: converted.messages, + tools: convertTools(input.tools), + system, + max_tokens: clampMaxTokens(input.max_tokens ?? input.max_completion_tokens), + stream: true, + }, + }; +} + +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 { + 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 applyEventToAggregate(event: JsonRecord, state: AggregateState): void { + 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: stringValue(event.toolName) || stringValue(event.name) || "", + arguments: JSON.stringify(args), + }, + }); + break; + } + case "finish": + state.finishReason = mapFinishReason(event.finishReason); + state.usage = isRecord(event.totalUsage) ? event.totalUsage : null; + break; + } +} + +function applyEventToAggregateOrThrow(event: JsonRecord, state: AggregateState): 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); +} + +function usageFromCommandCode(usage: JsonRecord | null) { + if (!usage) return undefined; + const details = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : {}; + const prompt = + (numberValue(usage.inputTokens) || 0) + (numberValue(details.cacheReadTokens) || 0); + const completion = numberValue(usage.outputTokens) || 0; + return { + prompt_tokens: prompt, + completion_tokens: completion, + total_tokens: prompt + completion, + }; +} + +function createStreamResponse( + upstream: Response, + model: string, + signal?: AbortSignal | null +): 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; + 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: stringValue(event.toolName) || stringValue(event.name) || "", + 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": { + state.finishReason = mapFinishReason(event.finishReason); + state.usage = isRecord(event.totalUsage) ? event.totalUsage : null; + controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason))); + 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 { + // Reader may already be released/cancelled. + } + } + }; + + 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 +): 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); + } + } + if (buffer.trim()) { + const event = parseStreamLine(buffer); + if (isRecord(event)) applyEventToAggregateOrThrow(event, state); + } + } finally { + try { + await reader.cancel(); + } catch { + // Reader may already be closed. + } + try { + reader.releaseLock(); + } catch { + // Reader may already be released. + } + } + + 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" }, + }); +} + +export class CommandCodeExecutor extends BaseExecutor { + constructor(provider = "command-code") { + super(provider, REGISTRY["command-code"]); + } + + buildUrl() { + const baseUrl = (this.config.baseUrl || "https://api.commandcode.ai").replace(/\/$/, ""); + return `${baseUrl}${this.config.chatPath || "/alpha/generate"}`; + } + + async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) { + const apiKey = credentials?.apiKey || credentials?.accessToken; + if (!apiKey) throw new Error("Command Code API key required"); + + const headers: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "x-command-code-version": COMMAND_CODE_VERSION, + "x-cli-environment": "production", + "x-project-slug": "pi-cc", + "x-taste-learning": "false", + "x-co-flag": "false", + "x-session-id": randomUUID(), + }; + mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); + + const transformedBody = buildCommandCodeBody(model, body); + const url = this.buildUrl(); + const upstream = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(transformedBody), + signal: signal || undefined, + }); + + if (!upstream.ok) { + const errorText = await upstream.text().catch(() => ""); + return { + response: new Response(errorText || `Command Code API error ${upstream.status}`, { + status: upstream.status, + statusText: upstream.statusText, + headers: upstream.headers, + }), + url, + headers, + transformedBody, + }; + } + + const response = stream + ? createStreamResponse(upstream, model, signal) + : await createJsonResponse(upstream, model, signal); + + return { response, url, headers, transformedBody }; + } +} diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 9dbf0a9349..512fb38c1b 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -19,6 +19,7 @@ import { ChatGptWebExecutor } from "./chatgpt-web.ts"; import { BlackboxWebExecutor } from "./blackbox-web.ts"; import { MuseSparkWebExecutor } from "./muse-spark-web.ts"; import { AzureOpenAIExecutor } from "./azure-openai.ts"; +import { CommandCodeExecutor } from "./commandCode.ts"; import { GitlabExecutor } from "./gitlab.ts"; import { NlpCloudExecutor } from "./nlpcloud.ts"; import { PetalsExecutor } from "./petals.ts"; @@ -39,6 +40,8 @@ const executors = { glmt: new GlmExecutor("glmt"), cu: new CursorExecutor(), // Alias for cursor "azure-openai": new AzureOpenAIExecutor(), + "command-code": new CommandCodeExecutor(), + cmd: new CommandCodeExecutor(), // Alias gitlab: new GitlabExecutor(), "gitlab-duo": new GitlabExecutor("gitlab-duo"), nlpcloud: new NlpCloudExecutor(), @@ -105,6 +108,7 @@ export { ChatGptWebExecutor } from "./chatgpt-web.ts"; export { BlackboxWebExecutor } from "./blackbox-web.ts"; export { MuseSparkWebExecutor } from "./muse-spark-web.ts"; export { AzureOpenAIExecutor } from "./azure-openai.ts"; +export { CommandCodeExecutor } from "./commandCode.ts"; export { GitlabExecutor } from "./gitlab.ts"; export { NlpCloudExecutor } from "./nlpcloud.ts"; export { PetalsExecutor } from "./petals.ts"; diff --git a/public/providers/command-code.svg b/public/providers/command-code.svg new file mode 100644 index 0000000000..8bb660108c --- /dev/null +++ b/public/providers/command-code.svg @@ -0,0 +1,42 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + + + + diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 142d9eb28e..e4b0a31340 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -556,6 +556,9 @@ interface AddApiKeyModalProps { isCompatible?: boolean; isAnthropic?: boolean; isCcCompatible?: boolean; + isCommandCode?: boolean; + commandCodeAuthState?: CommandCodeAuthFlowState; + onStartCommandCodeAuth?: () => void; onSave: (data: { name: string; apiKey?: string; @@ -566,6 +569,23 @@ interface AddApiKeyModalProps { onClose: () => void; } +type CommandCodeAuthFlowState = { + phase: + | "idle" + | "starting" + | "polling" + | "received" + | "applying" + | "applied" + | "expired" + | "error"; + state: string; + authUrl: string; + callbackUrl: string; + expiresAt: string | null; + message?: string; +}; + interface EditConnectionModalConnection { id?: string; name?: string; @@ -981,6 +1001,14 @@ export default function ProviderDetailPage() { const [showOAuthModal, _setShowOAuthModal] = useState(false); const [reauthConnection, setReauthConnection] = useState(null); const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); + const [commandCodeAuthState, setCommandCodeAuthState] = useState({ + phase: "idle", + state: "", + authUrl: "", + callbackUrl: "", + expiresAt: null, + message: "", + }); const [showEditModal, setShowEditModal] = useState(false); const [showEditNodeModal, setShowEditNodeModal] = useState(false); const [selectedConnection, setSelectedConnection] = useState(null); @@ -1027,8 +1055,11 @@ export default function ProviderDetailPage() { const [savingCodexGlobalFastServiceTier, setSavingCodexGlobalFastServiceTier] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [batchDeleting, setBatchDeleting] = useState(false); + const commandCodeAuthWindowRef = useRef(null); + const commandCodeAuthTimerRef = useRef(null); const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isCcCompatible = isClaudeCodeCompatibleProvider(providerId); + const isCommandCode = providerId === "command-code"; const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId) && !isClaudeCodeCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible; @@ -1436,6 +1467,257 @@ export default function ProviderDetailPage() { setShowAddApiKeyModal(true); }, [isOAuth]); + const clearCommandCodeAuthTimer = useCallback(() => { + if (commandCodeAuthTimerRef.current !== null) { + window.clearTimeout(commandCodeAuthTimerRef.current); + commandCodeAuthTimerRef.current = null; + } + }, []); + + useEffect(() => { + return () => { + clearCommandCodeAuthTimer(); + commandCodeAuthWindowRef.current?.close?.(); + }; + }, [clearCommandCodeAuthTimer]); + + const handleCloseAddApiKeyModal = useCallback(() => { + clearCommandCodeAuthTimer(); + commandCodeAuthWindowRef.current?.close?.(); + commandCodeAuthWindowRef.current = null; + setCommandCodeAuthState({ + phase: "idle", + state: "", + authUrl: "", + callbackUrl: "", + expiresAt: null, + message: "", + }); + setShowAddApiKeyModal(false); + }, [clearCommandCodeAuthTimer]); + + const handleCommandCodeAuthApply = useCallback( + async (state: string, connectionId?: string, name?: string, setDefault?: boolean) => { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "applying", + message: "Applying browser-approved key…", + })); + + try { + const res = await fetch("/api/providers/command-code/auth/apply", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ state, connectionId, name, setDefault }), + }); + const data = await res.json().catch(() => ({})); + + if (!res.ok) { + const errorMessage = data.error || "Failed to apply Command Code auth"; + setCommandCodeAuthState((current) => ({ + ...current, + phase: "error", + message: errorMessage, + })); + notify.error(errorMessage); + return false; + } + + setCommandCodeAuthState((current) => ({ + ...current, + phase: "applied", + message: "Command Code connected", + })); + commandCodeAuthWindowRef.current?.close?.(); + commandCodeAuthWindowRef.current = null; + await fetchConnections(); + handleCloseAddApiKeyModal(); + notify.success("Command Code connection added"); + return true; + } catch (error) { + console.error("Error applying Command Code auth:", error); + setCommandCodeAuthState((current) => ({ + ...current, + phase: "error", + message: "Failed to apply Command Code auth", + })); + notify.error("Failed to apply Command Code auth"); + return false; + } + }, + [fetchConnections, handleCloseAddApiKeyModal, notify] + ); + + const handleStartCommandCodeAuth = useCallback(async () => { + if (commandCodeAuthState.phase === "starting" || commandCodeAuthState.phase === "polling") { + return; + } + + clearCommandCodeAuthTimer(); + commandCodeAuthWindowRef.current?.close?.(); + + const popup = window.open("about:blank", "_blank"); + setCommandCodeAuthState({ + phase: "starting", + state: "", + authUrl: "", + callbackUrl: "", + expiresAt: null, + message: "Opening Command Code Studio…", + }); + + try { + const res = await fetch("/api/providers/command-code/auth/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + const data = await res.json().catch(() => ({})); + + if (!res.ok || !data.state || !data.authUrl) { + const errorMessage = data.error || "Failed to start Command Code auth"; + setCommandCodeAuthState((current) => ({ + ...current, + phase: "error", + message: errorMessage, + })); + notify.error(errorMessage); + popup?.close?.(); + return; + } + + setCommandCodeAuthState({ + phase: "polling", + state: data.state, + authUrl: data.authUrl, + callbackUrl: data.callbackUrl || "", + expiresAt: data.expiresAt || null, + message: "Open the auth URL, approve access, then paste the returned key/JSON/URL below…", + }); + + if (popup) { + try { + popup.opener = null; + } catch { + // Ignore opener cleanup failures. + } + popup.location.href = data.authUrl; + commandCodeAuthWindowRef.current = popup; + } else { + const fallbackPopup = window.open(data.authUrl, "_blank", "noopener,noreferrer"); + if (!fallbackPopup) { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "error", + message: "Popup blocked. Please allow popups and try Command Code Connect again.", + })); + notify.error("Popup blocked. Please allow popups and try Command Code Connect again."); + return; + } + commandCodeAuthWindowRef.current = fallbackPopup; + } + + const deadline = data.expiresAt ? new Date(data.expiresAt).getTime() : Date.now() + 180000; + const poll = async () => { + if (Date.now() >= deadline) { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "expired", + message: "Command Code link expired", + })); + commandCodeAuthWindowRef.current?.close?.(); + commandCodeAuthWindowRef.current = null; + notify.error("Command Code auth expired"); + clearCommandCodeAuthTimer(); + return; + } + + try { + const statusRes = await fetch( + `/api/providers/command-code/auth/status?state=${encodeURIComponent(data.state)}`, + { method: "GET", cache: "no-store" } + ); + const statusData = await statusRes.json().catch(() => ({})); + const status = String(statusData.status || statusData.state || statusData.phase || "") + .toLowerCase() + .trim(); + + if (status === "expired") { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "expired", + message: "Command Code link expired", + })); + commandCodeAuthWindowRef.current?.close?.(); + commandCodeAuthWindowRef.current = null; + notify.error("Command Code auth expired"); + clearCommandCodeAuthTimer(); + return; + } + + if (status === "applied") { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "applied", + message: "Command Code connected", + })); + commandCodeAuthWindowRef.current?.close?.(); + commandCodeAuthWindowRef.current = null; + await fetchConnections(); + handleCloseAddApiKeyModal(); + notify.success("Command Code connection added"); + clearCommandCodeAuthTimer(); + return; + } + + if (status === "received") { + setCommandCodeAuthState((current) => ({ + ...current, + phase: "received", + message: "Browser approved, applying…", + })); + clearCommandCodeAuthTimer(); + await handleCommandCodeAuthApply( + data.state, + statusData.connectionId, + statusData.name, + statusData.setDefault + ); + return; + } + } catch { + // Keep polling until the contract reports a terminal state or timeout. + } + + commandCodeAuthTimerRef.current = window.setTimeout(poll, 2000); + }; + + commandCodeAuthTimerRef.current = window.setTimeout(poll, 1000); + } catch (error) { + console.error("Error starting Command Code auth:", error); + setCommandCodeAuthState((current) => ({ + ...current, + phase: "error", + message: "Failed to start Command Code auth", + })); + notify.error("Failed to start Command Code auth"); + popup?.close?.(); + commandCodeAuthWindowRef.current = null; + clearCommandCodeAuthTimer(); + } + }, [ + clearCommandCodeAuthTimer, + handleCloseAddApiKeyModal, + commandCodeAuthState.phase, + fetchConnections, + handleCommandCodeAuthApply, + notify, + ]); + + const handleOpenCommandCodeConnect = useCallback(() => { + setShowAddApiKeyModal(true); + void handleStartCommandCodeAuth(); + }, [handleStartCommandCodeAuth]); + const handleSaveApiKey = async (formData) => { try { const res = await fetch("/api/providers", { @@ -2950,13 +3232,44 @@ export default function ProviderDetailPage() { )} {!isCompatible ? ( <> - - {providerId === "qoder" && ( - + {isCommandCode ? ( + <> + + + + ) : ( + <> + + {providerId === "qoder" && ( + + )} + )} ) : ( @@ -2980,13 +3293,38 @@ export default function ProviderDetailPage() {

{t("addFirstConnectionHint")}

{!isCompatible && (
- - {providerId === "qoder" && ( - + {isCommandCode ? ( + <> + + + + ) : ( + <> + + {providerId === "qoder" && ( + + )} + )}
)} @@ -3409,8 +3747,11 @@ export default function ProviderDetailPage() { isCompatible={isCompatible} isAnthropic={isAnthropicProtocolCompatible} isCcCompatible={isCcCompatible} + isCommandCode={isCommandCode} + commandCodeAuthState={commandCodeAuthState} + onStartCommandCodeAuth={handleStartCommandCodeAuth} onSave={handleSaveApiKey} - onClose={() => setShowAddApiKeyModal(false)} + onClose={handleCloseAddApiKeyModal} /> )} {!isUpstreamProxyProvider && ( @@ -5715,6 +6056,52 @@ function formatExcludedModelsInput(value: unknown): string { .join(", "); } +function extractCommandCodeCredentialInput(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return ""; + + try { + const parsed = JSON.parse(trimmed) as unknown; + if (parsed && typeof parsed === "object") { + const record = parsed as Record; + const direct = record.apiKey || record.api_key || record.key || record.token; + if (typeof direct === "string" && direct.trim()) return direct.trim(); + const nested = record.data; + if (nested && typeof nested === "object") { + const nestedRecord = nested as Record; + const nestedKey = nestedRecord.apiKey || nestedRecord.api_key || nestedRecord.key; + if (typeof nestedKey === "string" && nestedKey.trim()) return nestedKey.trim(); + } + } + } catch { + // Not JSON; continue with URL/raw parsing. + } + + try { + const url = new URL(trimmed); + const key = + url.searchParams.get("apiKey") || + url.searchParams.get("api_key") || + url.searchParams.get("key") || + url.searchParams.get("token"); + if (key?.trim()) return key.trim(); + const hash = url.hash.replace(/^#/, ""); + if (hash) { + const hashParams = new URLSearchParams(hash); + const hashKey = + hashParams.get("apiKey") || + hashParams.get("api_key") || + hashParams.get("key") || + hashParams.get("token"); + if (hashKey?.trim()) return hashKey.trim(); + } + } catch { + // Not a URL; use the raw value. + } + + return trimmed; +} + function AddApiKeyModal({ isOpen, provider, @@ -5722,6 +6109,9 @@ function AddApiKeyModal({ isCompatible, isAnthropic, isCcCompatible, + isCommandCode, + commandCodeAuthState, + onStartCommandCodeAuth, onSave, onClose, }: AddApiKeyModalProps) { @@ -5744,6 +6134,18 @@ function AddApiKeyModal({ const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb; const isPetals = provider === "petals"; const apiKeyOptional = isSearxng || isPetals || isLocalSelfHostedProvider; + const commandCodeAuthPhaseLabel = commandCodeAuthState + ? { + idle: "Ready", + starting: "Starting…", + polling: "Waiting for browser…", + received: "Browser approved", + applying: "Applying key…", + applied: "Connected", + expired: "Link expired", + error: "Connection failed", + }[commandCodeAuthState.phase] + : null; const [formData, setFormData] = useState({ name: "", @@ -5767,6 +6169,7 @@ function AddApiKeyModal({ const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); + const [copiedCommandCodeField, setCopiedCommandCodeField] = useState(null); const apiCredentialLabel = isQoder ? t("personalAccessTokenLabel") : isWebSessionProvider @@ -5811,12 +6214,15 @@ function AddApiKeyModal({ setValidating(true); setSaveError(null); try { + const credentialInput = isCommandCode + ? extractCommandCodeCredentialInput(formData.apiKey) + : formData.apiKey; const res = await fetch("/api/providers/validate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, - apiKey: formData.apiKey, + apiKey: credentialInput, validationModelId: formData.validationModelId || undefined, customUserAgent: formData.customUserAgent.trim() || undefined, baseUrl: formData.baseUrl.trim() || undefined, @@ -5832,8 +6238,22 @@ function AddApiKeyModal({ } }; + const copyCommandCodeValue = async (value: string | undefined, key: string) => { + if (!value) return; + try { + await navigator.clipboard.writeText(value); + setCopiedCommandCodeField(key); + window.setTimeout(() => setCopiedCommandCodeField(null), 1500); + } catch { + setSaveError("Copy failed. Select the text and copy it manually."); + } + }; + const handleSubmit = async () => { - if (!provider || (!isCompatible && !apiKeyOptional && !formData.apiKey)) return; + const credentialInput = isCommandCode + ? extractCommandCodeCredentialInput(formData.apiKey) + : formData.apiKey; + if (!provider || (!isCompatible && !apiKeyOptional && !credentialInput)) return; setSaving(true); setSaveError(null); @@ -5863,7 +6283,7 @@ function AddApiKeyModal({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, - apiKey: formData.apiKey, + apiKey: credentialInput, validationModelId: formData.validationModelId || undefined, customUserAgent: formData.customUserAgent.trim() || undefined, baseUrl: formData.baseUrl.trim() || undefined, @@ -5883,7 +6303,7 @@ function AddApiKeyModal({ } if (!isValid) { - if (apiKeyOptional && !formData.apiKey) { + if (apiKeyOptional && !credentialInput) { // Bypass validation block for local/optional providers when no key is provided console.debug("Validation failed but apiKey is optional; proceeding to save."); } else { @@ -5926,7 +6346,7 @@ function AddApiKeyModal({ const payload = { name: formData.name, - apiKey: formData.apiKey.trim() || undefined, + apiKey: credentialInput.trim() || undefined, priority: formData.priority, testStatus: "active", providerSpecificData: @@ -5961,6 +6381,84 @@ function AddApiKeyModal({ )} + {isCommandCode && onStartCommandCodeAuth && ( +
+
+ + open_in_new + +
+

Browser/manual connect

+

+ Open Command Code Studio, then paste the returned key/JSON/URL into the API key + field below. +

+ {commandCodeAuthState?.message && ( +

+ {commandCodeAuthPhaseLabel}: {commandCodeAuthState.message} +

+ )} + {commandCodeAuthState?.authUrl && ( +
+
+

Auth URL

+
+ +
+
+ {commandCodeAuthState.callbackUrl && ( +
+

Callback URL

+
+ +
+
+ )} +
+ )} +
+ +
+
+ )} | null +): Record | null { + if (!connection) return null; + const result = { ...connection }; + delete result.apiKey; + delete result.accessToken; + delete result.refreshToken; + delete result.idToken; + if (result.providerSpecificData) { + result.providerSpecificData = sanitizeProviderSpecificDataForResponse( + result.providerSpecificData + ); + } + return result; +} + +export async function POST(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + let body: unknown; + try { + body = await request.json(); + } catch { + return noStoreJson({ error: "Invalid JSON body" }, { status: 400 }); + } + + const parsed = commandCodeApplySchema.safeParse(body); + if (!parsed.success) return noStoreJson({ error: "Invalid apply payload" }, { status: 400 }); + + let existing: Record | null = null; + if (parsed.data.connectionId) { + existing = (await getProviderConnectionById(parsed.data.connectionId)) as Record< + string, + unknown + > | null; + if (!existing || existing.provider !== "command-code" || existing.authType !== "apikey") { + return noStoreJson({ error: "Command Code API-key connection not found" }, { status: 404 }); + } + } + + const consumed = consumeCommandCodeAuthSecret(stateHashFromState(parsed.data.state)); + if (!consumed) { + return noStoreJson( + { error: "No received Command Code API key for this state" }, + { status: 409 } + ); + } + + let connection: Record | null; + if (parsed.data.connectionId && existing) { + connection = (await updateProviderConnection(parsed.data.connectionId, { + apiKey: consumed.apiKey, + name: parsed.data.name || existing.name || consumed.metadata?.keyName || "Command Code", + isActive: true, + testStatus: "unknown", + providerSpecificData: { + ...((existing.providerSpecificData as Record | null) || {}), + authAssist: { + userId: consumed.metadata?.userId, + userName: consumed.metadata?.userName, + keyName: consumed.metadata?.keyName, + appliedAt: consumed.appliedAt, + }, + }, + ...(parsed.data.setDefault ? { priority: 1 } : {}), + })) as Record | null; + } else { + connection = (await createProviderConnection({ + provider: "command-code", + authType: "apikey", + name: parsed.data.name || consumed.metadata?.keyName || "Command Code", + apiKey: consumed.apiKey, + priority: parsed.data.setDefault ? 1 : undefined, + isActive: true, + testStatus: "unknown", + providerSpecificData: { + authAssist: { + userId: consumed.metadata?.userId, + userName: consumed.metadata?.userName, + keyName: consumed.metadata?.keyName, + appliedAt: consumed.appliedAt, + }, + }, + })) as Record | null; + } + + return noStoreJson({ connection: safeConnection(connection), status: "applied" }); +} diff --git a/src/app/api/providers/command-code/auth/callback/route.ts b/src/app/api/providers/command-code/auth/callback/route.ts new file mode 100644 index 0000000000..361d0716a7 --- /dev/null +++ b/src/app/api/providers/command-code/auth/callback/route.ts @@ -0,0 +1,67 @@ +import { markCommandCodeAuthSessionReceived } from "@/lib/db/commandCodeAuth"; + +import { + callbackCorsHeaders, + commandCodeCallbackSchema, + MAX_CALLBACK_BODY_BYTES, + noStoreJson, + readJsonBodyWithLimit, + rejectDisallowedCallbackOrigin, + stateHashFromState, +} from "../shared"; + +export async function OPTIONS(request: Request) { + return new Response(null, { status: 204, headers: callbackCorsHeaders(request) }); +} + +export async function POST(request: Request) { + const originError = rejectDisallowedCallbackOrigin(request); + if (originError) return originError; + + let body: unknown; + try { + body = await readJsonBodyWithLimit(request, MAX_CALLBACK_BODY_BYTES); + } catch (error) { + const isTooLarge = error instanceof Error && error.message === "BODY_TOO_LARGE"; + return noStoreJson( + { success: false, error: isTooLarge ? "Request body too large" : "Invalid JSON body" }, + { status: isTooLarge ? 413 : 400, headers: callbackCorsHeaders(request) } + ); + } + + const parsed = commandCodeCallbackSchema.safeParse(body); + if (!parsed.success) { + return noStoreJson( + { success: false, error: "Invalid callback payload" }, + { status: 400, headers: callbackCorsHeaders(request) } + ); + } + + const session = markCommandCodeAuthSessionReceived({ + stateHash: stateHashFromState(parsed.data.state), + apiKey: parsed.data.apiKey, + metadata: { + userId: parsed.data.userId, + userName: parsed.data.userName, + keyName: parsed.data.keyName, + }, + }); + + if (!session || session.status !== "received") { + return noStoreJson( + { success: false, error: "Invalid or expired state" }, + { status: 400, headers: callbackCorsHeaders(request) } + ); + } + + return noStoreJson( + { + success: true, + ok: true, + status: session.status, + expiresAt: session.expiresAt, + metadata: session.metadata, + }, + { headers: callbackCorsHeaders(request) } + ); +} diff --git a/src/app/api/providers/command-code/auth/shared.ts b/src/app/api/providers/command-code/auth/shared.ts new file mode 100644 index 0000000000..6755f6753d --- /dev/null +++ b/src/app/api/providers/command-code/auth/shared.ts @@ -0,0 +1,120 @@ +import { Buffer } from "node:buffer"; +import { randomBytes } from "crypto"; + +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { hashCommandCodeAuthState } from "@/lib/db/commandCodeAuth"; + +export const COMMAND_CODE_AUTH_TTL_MS = 15 * 60 * 1000; +export const COMMAND_CODE_STUDIO_AUTH_URL = "https://commandcode.ai/studio/auth/cli"; +export const MAX_CALLBACK_BODY_BYTES = 10 * 1024; +export const COMMAND_CODE_CLI_CALLBACK_PORTS = [ + 5959, 5960, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, +] as const; + +const LOCAL_CALLBACK_ORIGIN = "http://localhost:3000"; +const PRODUCTION_CALLBACK_ORIGINS = ["https://commandcode.ai", "https://staging.commandcode.ai"]; + +export const commandCodeCallbackSchema = z.object({ + apiKey: z.string().trim().min(1).max(4096), + state: z.string().trim().min(32).max(512), + userId: z.string().trim().max(256).optional(), + userName: z.string().trim().max(256).optional(), + keyName: z.string().trim().max(256).optional(), +}); + +export const commandCodeStateSchema = z.object({ + state: z.string().trim().min(32).max(512), +}); + +export const commandCodeApplySchema = commandCodeStateSchema.extend({ + connectionId: z.string().trim().min(1).max(256).optional(), + name: z.string().trim().min(1).max(256).optional(), + setDefault: z.boolean().optional(), +}); + +export function generateCommandCodeState(): string { + return randomBytes(32).toString("base64url"); +} + +export function stateHashFromState(state: string): string { + return hashCommandCodeAuthState(state); +} + +export function noStoreJson(body: unknown, init: ResponseInit = {}): NextResponse { + return NextResponse.json(body, { + ...init, + headers: { + "Cache-Control": "no-store", + ...(init.headers || {}), + }, + }); +} + +export function getAllowedCallbackOrigin(origin: string | null): string | null { + const allowed = + process.env.NODE_ENV === "production" + ? PRODUCTION_CALLBACK_ORIGINS + : [...PRODUCTION_CALLBACK_ORIGINS, LOCAL_CALLBACK_ORIGIN]; + return origin && allowed.includes(origin) ? origin : null; +} + +export function callbackCorsHeaders(request: Request): HeadersInit { + const requestHeaders = request.headers.get("access-control-request-headers") || "content-type"; + const origin = getAllowedCallbackOrigin(request.headers.get("origin")); + const headers: Record = { + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": requestHeaders, + "Access-Control-Allow-Private-Network": "true", + "Content-Type": "application/json", + "Cache-Control": "no-store", + Vary: "Origin, Access-Control-Request-Headers", + }; + if (origin) headers["Access-Control-Allow-Origin"] = origin; + return headers; +} + +export function rejectDisallowedCallbackOrigin(request: Request): Response | null { + const origin = request.headers.get("origin"); + if (!origin || getAllowedCallbackOrigin(origin)) return null; + return new Response(JSON.stringify({ success: false, error: "Origin not allowed" }), { + status: 403, + headers: callbackCorsHeaders(request), + }); +} + +export async function readJsonBodyWithLimit(request: Request, maxBytes: number): Promise { + const reader = request.body?.getReader(); + if (!reader) return request.json(); + + const chunks: Uint8Array[] = []; + let total = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (!value) continue; + total += value.byteLength; + if (total > maxBytes) { + await reader.cancel(); + throw new Error("BODY_TOO_LARGE"); + } + chunks.push(value); + } + + const body = new TextDecoder().decode(Buffer.concat(chunks)); + return JSON.parse(body); +} + +export function buildCommandCodeCliCallbackUrl(): string { + const configuredPort = process.env.COMMAND_CODE_CALLBACK_PORT || ""; + const port = /^\d+$/.test(configuredPort) + ? Number.parseInt(configuredPort, 10) + : COMMAND_CODE_CLI_CALLBACK_PORTS[0]; + const safePort = COMMAND_CODE_CLI_CALLBACK_PORTS.includes( + port as (typeof COMMAND_CODE_CLI_CALLBACK_PORTS)[number] + ) + ? port + : COMMAND_CODE_CLI_CALLBACK_PORTS[0]; + return `http://localhost:${safePort}/callback`; +} diff --git a/src/app/api/providers/command-code/auth/start/route.ts b/src/app/api/providers/command-code/auth/start/route.ts new file mode 100644 index 0000000000..dfa31dfcec --- /dev/null +++ b/src/app/api/providers/command-code/auth/start/route.ts @@ -0,0 +1,28 @@ +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { createPendingCommandCodeAuthSession } from "@/lib/db/commandCodeAuth"; + +import { + buildCommandCodeCliCallbackUrl, + COMMAND_CODE_AUTH_TTL_MS, + COMMAND_CODE_STUDIO_AUTH_URL, + generateCommandCodeState, + noStoreJson, + stateHashFromState, +} from "../shared"; + +export async function POST(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + const state = generateCommandCodeState(); + const expiresAt = new Date(Date.now() + COMMAND_CODE_AUTH_TTL_MS).toISOString(); + const stateHash = stateHashFromState(state); + createPendingCommandCodeAuthSession({ stateHash, expiresAt }); + + const callbackUrl = buildCommandCodeCliCallbackUrl(); + const authUrl = `${COMMAND_CODE_STUDIO_AUTH_URL}?callback=${encodeURIComponent( + callbackUrl + )}&state=${encodeURIComponent(state)}`; + + return noStoreJson({ state, authUrl, callbackUrl, expiresAt, mode: "manual" }); +} diff --git a/src/app/api/providers/command-code/auth/status/route.ts b/src/app/api/providers/command-code/auth/status/route.ts new file mode 100644 index 0000000000..4c71096b9e --- /dev/null +++ b/src/app/api/providers/command-code/auth/status/route.ts @@ -0,0 +1,38 @@ +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { getCommandCodeAuthSessionSafeStatus } from "@/lib/db/commandCodeAuth"; + +import { commandCodeStateSchema, noStoreJson, stateHashFromState } from "../shared"; + +async function readState(request: Request): Promise { + const urlState = new URL(request.url).searchParams.get("state"); + if (urlState) return urlState; + try { + const parsed = commandCodeStateSchema.safeParse(await request.json()); + return parsed.success ? parsed.data.state : null; + } catch { + return null; + } +} + +async function handle(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + + const state = await readState(request); + const parsed = commandCodeStateSchema.safeParse({ state }); + if (!parsed.success) return noStoreJson({ error: "Invalid state" }, { status: 400 }); + + const session = getCommandCodeAuthSessionSafeStatus(stateHashFromState(parsed.data.state)); + if (!session) return noStoreJson({ status: "not_found" }, { status: 404 }); + + return noStoreJson({ + status: session.status, + metadata: session.metadata, + expiresAt: session.expiresAt, + receivedAt: session.receivedAt, + appliedAt: session.appliedAt, + }); +} + +export const GET = handle; +export const POST = handle; diff --git a/src/lib/db/commandCodeAuth.ts b/src/lib/db/commandCodeAuth.ts new file mode 100644 index 0000000000..33961fdc33 --- /dev/null +++ b/src/lib/db/commandCodeAuth.ts @@ -0,0 +1,213 @@ +import { createHash, randomUUID } from "crypto"; + +import { getDbInstance, rowToCamel } from "./core"; +import { decrypt, encrypt } from "./encryption"; + +export type CommandCodeAuthStatus = "pending" | "received" | "applied" | "expired"; + +export interface CommandCodeAuthMetadata { + userId?: string; + userName?: string; + keyName?: string; + receivedAt?: string; +} + +export interface CommandCodeAuthSafeStatus { + id: string; + stateHash: string; + status: CommandCodeAuthStatus; + metadata: CommandCodeAuthMetadata | null; + createdAt: string; + expiresAt: string; + receivedAt: string | null; + appliedAt: string | null; + updatedAt: string; +} + +export interface ConsumedCommandCodeAuthSecret extends CommandCodeAuthSafeStatus { + apiKey: string; +} + +type DbRunResult = { changes?: number }; +type DbStatement = { + get: (...params: unknown[]) => TRow | undefined; + all: (...params: unknown[]) => TRow[]; + run: (...params: unknown[]) => DbRunResult; +}; +type DbLike = { + prepare: (sql: string) => DbStatement; + transaction: unknown>(fn: T) => T; +}; + +type AuthSessionRow = { + id: string; + state_hash: string; + status: CommandCodeAuthStatus; + encrypted_api_key?: string | null; + metadata_json?: string | null; + created_at: string; + expires_at: string; + received_at?: string | null; + applied_at?: string | null; + updated_at: string; +}; + +function db(): DbLike { + return getDbInstance() as unknown as DbLike; +} + +export function hashCommandCodeAuthState(state: string): string { + return createHash("sha256").update(state, "utf8").digest("hex"); +} + +function nowIso(): string { + return new Date().toISOString(); +} + +function parseMetadata(value: string | null | undefined): CommandCodeAuthMetadata | null { + if (!value) return null; + try { + const parsed = JSON.parse(value) as CommandCodeAuthMetadata; + return parsed && typeof parsed === "object" ? parsed : null; + } catch { + return null; + } +} + +function toSafeStatus(row: AuthSessionRow): CommandCodeAuthSafeStatus { + const camel = rowToCamel(row) as Record; + return { + id: String(camel.id), + stateHash: String(camel.stateHash), + status: camel.status as CommandCodeAuthStatus, + metadata: parseMetadata(camel.metadataJson as string | null | undefined), + createdAt: String(camel.createdAt), + expiresAt: String(camel.expiresAt), + receivedAt: (camel.receivedAt as string | null | undefined) ?? null, + appliedAt: (camel.appliedAt as string | null | undefined) ?? null, + updatedAt: String(camel.updatedAt), + }; +} + +function markExpiredForState(stateHash: string, now = nowIso()): void { + db() + .prepare( + `UPDATE command_code_auth_sessions + SET status = 'expired', updated_at = ? + WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at <= ?` + ) + .run(now, stateHash, now); +} + +export function createPendingCommandCodeAuthSession(input: { + stateHash: string; + expiresAt: string; +}): CommandCodeAuthSafeStatus { + const id = randomUUID(); + const now = nowIso(); + db() + .prepare( + `INSERT INTO command_code_auth_sessions ( + id, state_hash, status, encrypted_api_key, metadata_json, + created_at, expires_at, received_at, applied_at, updated_at + ) VALUES (?, ?, 'pending', NULL, NULL, ?, ?, NULL, NULL, ?)` + ) + .run(id, input.stateHash, now, input.expiresAt, now); + + const row = db() + .prepare("SELECT * FROM command_code_auth_sessions WHERE id = ?") + .get(id); + if (!row) throw new Error("Failed to create Command Code auth session"); + return toSafeStatus(row); +} + +export function markCommandCodeAuthSessionReceived(input: { + stateHash: string; + apiKey: string; + metadata?: CommandCodeAuthMetadata; +}): CommandCodeAuthSafeStatus | null { + const now = nowIso(); + markExpiredForState(input.stateHash, now); + const metadata: CommandCodeAuthMetadata = { + ...(input.metadata || {}), + receivedAt: now, + }; + const encryptedApiKey = encrypt(input.apiKey); + db() + .prepare( + `UPDATE command_code_auth_sessions + SET status = 'received', encrypted_api_key = ?, metadata_json = ?, received_at = ?, updated_at = ? + WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at > ?` + ) + .run(encryptedApiKey, JSON.stringify(metadata), now, now, input.stateHash, now); + + return getCommandCodeAuthSessionSafeStatus(input.stateHash); +} + +export function getCommandCodeAuthSessionSafeStatus( + stateHash: string +): CommandCodeAuthSafeStatus | null { + markExpiredForState(stateHash); + const row = db() + .prepare("SELECT * FROM command_code_auth_sessions WHERE state_hash = ?") + .get(stateHash); + return row ? toSafeStatus(row) : null; +} + +export function consumeCommandCodeAuthSecret( + stateHash: string +): ConsumedCommandCodeAuthSecret | null { + const database = db(); + return database.transaction(() => { + const now = nowIso(); + database + .prepare( + `UPDATE command_code_auth_sessions + SET status = 'expired', updated_at = ? + WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at <= ?` + ) + .run(now, stateHash, now); + + const row = database + .prepare( + `SELECT * FROM command_code_auth_sessions + WHERE state_hash = ? AND status = 'received' AND expires_at > ? AND encrypted_api_key IS NOT NULL` + ) + .get(stateHash, now); + if (!row?.encrypted_api_key) return null; + + const apiKey = decrypt(row.encrypted_api_key); + if (!apiKey) return null; + + const result = database + .prepare( + `UPDATE command_code_auth_sessions + SET status = 'applied', encrypted_api_key = NULL, applied_at = ?, updated_at = ? + WHERE id = ? AND status = 'received'` + ) + .run(now, now, row.id); + if (!result.changes) return null; + + return { + ...toSafeStatus({ + ...row, + status: "applied", + encrypted_api_key: null, + applied_at: now, + updated_at: now, + }), + apiKey, + }; + })() as ConsumedCommandCodeAuthSecret | null; +} + +export function cleanupExpiredCommandCodeAuthSessions(now = nowIso()): number { + const result = db() + .prepare( + `UPDATE command_code_auth_sessions + SET status = 'expired', updated_at = ? + WHERE status IN ('pending', 'received') AND expires_at <= ?` + ) + .run(now, now); + return result.changes ?? 0; +} diff --git a/src/lib/db/migrations/055_command_code_auth_sessions.sql b/src/lib/db/migrations/055_command_code_auth_sessions.sql new file mode 100644 index 0000000000..f1e927443f --- /dev/null +++ b/src/lib/db/migrations/055_command_code_auth_sessions.sql @@ -0,0 +1,19 @@ +-- Migration 055: Pending browser-assisted Command Code auth sessions +CREATE TABLE IF NOT EXISTS command_code_auth_sessions ( + id TEXT PRIMARY KEY, + state_hash TEXT NOT NULL UNIQUE, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'received', 'applied', 'expired')), + encrypted_api_key TEXT, + metadata_json TEXT, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + received_at TEXT, + applied_at TEXT, + updated_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_command_code_auth_sessions_state_hash + ON command_code_auth_sessions(state_hash); + +CREATE INDEX IF NOT EXISTS idx_command_code_auth_sessions_status_expires + ON command_code_auth_sessions(status, expires_at); diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 505eb09522..55a6b7763c 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -1,6 +1,7 @@ -import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; +import { randomUUID } from "node:crypto"; import { getEmbeddingProvider } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { getRerankProvider } from "@omniroute/open-sse/config/rerankRegistry.ts"; +import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; import { buildClaudeCodeCompatibleHeaders, buildClaudeCodeCompatibleValidationPayload, @@ -398,6 +399,53 @@ async function validateDirectChatProvider({ url, headers, body, providerSpecific } } +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 url = `${baseUrl}${chatPath.startsWith("/") ? chatPath : `/${chatPath}`}`; + + return validateDirectChatProvider({ + url, + providerSpecificData, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "x-command-code-version": "0.24.1", + "x-cli-environment": "production", + "x-project-slug": "pi-cc", + "x-taste-learning": "false", + "x-co-flag": "false", + "x-session-id": randomUUID(), + }, + body: { + config: { + workingDir: "/workspace", + date: new Date().toISOString().slice(0, 10), + environment: "omniroute-validation", + structure: [], + isGitRepo: false, + currentBranch: "", + mainBranch: "", + gitStatus: "", + recentCommits: [], + }, + memory: "", + taste: "", + skills: null, + permissionMode: "standard", + params: { + model: providerSpecificData?.validationModelId || entry?.models?.[0]?.id || "gpt-5.4-mini", + messages: [{ role: "user", content: "test" }], + tools: [], + system: "", + max_tokens: 1, + stream: true, + }, + }, + }); +} + async function validateClarifaiProvider({ apiKey, providerSpecificData = {} }: any) { const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.clarifai.com/v2/ext/openai/v1"; @@ -3005,6 +3053,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi const SPECIALTY_VALIDATORS = { qoder: ({ apiKey, providerSpecificData }: any) => validateQoderCliPat({ apiKey, providerSpecificData }), + "command-code": validateCommandCodeProvider, deepgram: validateDeepgramProvider, assemblyai: validateAssemblyAIProvider, nanobanana: validateNanoBananaProvider, diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index ea5892595f..a763ee0088 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -77,6 +77,7 @@ const KNOWN_SVGS = new Set([ "brave-search", "cartesia", "clarifai", + "command-code", "docker-model-runner", "droid", "gemini-cli", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 1f06a86ebe..490f54bc6f 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -184,6 +184,18 @@ export const APIKEY_PROVIDERS = { freeNote: "$200 free credits on signup - multi-model routing gateway", apiHint: "Get $200 free credits at https://agentrouter.org/register — no credit card required.", }, + "command-code": { + id: "command-code", + alias: "cmd", + name: "Command Code", + icon: "terminal", + color: "#111827", + textIcon: "CC", + website: "https://commandcode.ai/", + authHint: + "Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint.", + apiHint: "Create or copy an API key from Command Code, then paste it here as a Bearer token.", + }, openrouter: { id: "openrouter", alias: "openrouter", diff --git a/tests/unit/command-code-auth-assist.test.ts b/tests/unit/command-code-auth-assist.test.ts new file mode 100644 index 0000000000..f8ca2c8a66 --- /dev/null +++ b/tests/unit/command-code-auth-assist.test.ts @@ -0,0 +1,231 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-command-code-auth-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.STORAGE_ENCRYPTION_KEY = "test-command-code-auth-encryption-key"; +delete process.env.INITIAL_PASSWORD; + +const core = await import("../../src/lib/db/core.ts"); +const startRoute = await import("../../src/app/api/providers/command-code/auth/start/route.ts"); +const callbackRoute = + await import("../../src/app/api/providers/command-code/auth/callback/route.ts"); +const statusRoute = await import("../../src/app/api/providers/command-code/auth/status/route.ts"); +const applyRoute = await import("../../src/app/api/providers/command-code/auth/apply/route.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); + +function resetDb() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +function jsonRequest(url: string, body: unknown, headers: HeadersInit = {}) { + return new Request(url, { + method: "POST", + headers: { "content-type": "application/json", ...headers }, + body: JSON.stringify(body), + }); +} + +test.beforeEach(() => { + delete process.env.OMNIROUTE_PUBLIC_BASE_URL; + delete process.env.OMNIROUTE_BASE_URL; + delete process.env.BASE_URL; + delete process.env.NEXT_PUBLIC_BASE_URL; + delete process.env.COMMAND_CODE_CALLBACK_PORT; + resetDb(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("Command Code auth assist start/callback/status/apply keeps state hash and key private", async () => { + const startResponse = await startRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/start", { + method: "POST", + headers: { origin: "http://localhost:20128" }, + }) + ); + assert.equal(startResponse.status, 200); + assert.equal(startResponse.headers.get("cache-control"), "no-store"); + const startBody = await startResponse.json(); + assert.equal(typeof startBody.state, "string"); + assert.ok(startBody.authUrl.startsWith("https://commandcode.ai/studio/auth/cli?")); + assert.ok(!("stateHash" in startBody)); + + const authUrl = new URL(startBody.authUrl); + const callbackUrl = authUrl.searchParams.get("callback"); + assert.ok(callbackUrl); + assert.equal(callbackUrl, startBody.callbackUrl); + assert.equal(callbackUrl, "http://localhost:5959/callback"); + assert.equal(startBody.mode, "manual"); + + const optionsResponse = await callbackRoute.OPTIONS( + new Request("http://localhost:20128/api/providers/command-code/auth/callback", { + method: "OPTIONS", + headers: { + origin: "https://commandcode.ai", + "access-control-request-headers": "content-type, x-command-code", + }, + }) + ); + assert.equal(optionsResponse.status, 204); + assert.equal( + optionsResponse.headers.get("access-control-allow-origin"), + "https://commandcode.ai" + ); + assert.equal(optionsResponse.headers.get("access-control-allow-methods"), "POST, OPTIONS"); + assert.equal(optionsResponse.headers.get("access-control-allow-private-network"), "true"); + assert.equal( + optionsResponse.headers.get("access-control-allow-headers"), + "content-type, x-command-code" + ); + + const callbackResponse = await callbackRoute.POST( + jsonRequest( + "http://localhost:20128/api/providers/command-code/auth/callback", + { + apiKey: "cc_test_secret", + state: startBody.state, + userId: "user-1", + userName: "Ada", + keyName: "Studio Key", + }, + { origin: "https://commandcode.ai" } + ) + ); + assert.equal(callbackResponse.status, 200); + const callbackBody = await callbackResponse.json(); + assert.equal(callbackBody.success, true); + + const statusResponse = await statusRoute.GET( + new Request( + `http://localhost:20128/api/providers/command-code/auth/status?state=${encodeURIComponent( + startBody.state + )}` + ) + ); + assert.equal(statusResponse.status, 200); + const statusBody = await statusResponse.json(); + assert.equal(statusBody.status, "received"); + assert.equal(statusBody.metadata.userName, "Ada"); + assert.ok(!JSON.stringify(statusBody).includes("cc_test_secret")); + assert.ok(!("stateHash" in statusBody)); + + const applyResponse = await applyRoute.POST( + jsonRequest("http://localhost:20128/api/providers/command-code/auth/apply", { + state: startBody.state, + name: "Command Code Studio", + setDefault: true, + }) + ); + assert.equal(applyResponse.status, 200); + const applyBody = await applyResponse.json(); + assert.equal(applyBody.status, "applied"); + assert.equal(applyBody.connection.provider, "command-code"); + assert.equal(applyBody.connection.authType, "apikey"); + assert.ok(!JSON.stringify(applyBody).includes("cc_test_secret")); + assert.ok(!("apiKey" in applyBody.connection)); + assert.ok(!("stateHash" in applyBody)); + + const connections = await providersDb.getProviderConnections({ provider: "command-code" }); + assert.equal(connections.length, 1); + assert.equal(connections[0].apiKey, "cc_test_secret"); + + const secondApplyResponse = await applyRoute.POST( + jsonRequest("http://localhost:20128/api/providers/command-code/auth/apply", { + state: startBody.state, + }) + ); + assert.equal(secondApplyResponse.status, 409); +}); + +test("Command Code auth assist keeps auth URL callback on CLI localhost contract", async () => { + process.env.OMNIROUTE_PUBLIC_BASE_URL = "https://omniroute.example.com/base-path"; + + const startResponse = await startRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/start", { + method: "POST", + headers: { origin: "http://localhost:20128" }, + }) + ); + assert.equal(startResponse.status, 200); + const startBody = await startResponse.json(); + const authUrl = new URL(startBody.authUrl); + + assert.equal(authUrl.searchParams.get("callback"), "http://localhost:5959/callback"); + assert.equal(startBody.callbackUrl, authUrl.searchParams.get("callback")); +}); + +test("Command Code auth assist allows only configured CLI callback port range", async () => { + process.env.COMMAND_CODE_CALLBACK_PORT = "5962"; + const configuredPortResponse = await startRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/start", { + method: "POST", + headers: { origin: "http://localhost:20128" }, + }) + ); + const configuredPortBody = await configuredPortResponse.json(); + assert.equal( + new URL(configuredPortBody.authUrl).searchParams.get("callback"), + "http://localhost:5962/callback" + ); + + resetDb(); + process.env.COMMAND_CODE_CALLBACK_PORT = "20128"; + const invalidPortResponse = await startRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/start", { + method: "POST", + headers: { origin: "http://localhost:20128" }, + }) + ); + const invalidPortBody = await invalidPortResponse.json(); + assert.equal( + new URL(invalidPortBody.authUrl).searchParams.get("callback"), + "http://localhost:5959/callback" + ); + + resetDb(); + process.env.COMMAND_CODE_CALLBACK_PORT = "5962abc"; + const partialPortResponse = await startRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/start", { + method: "POST", + headers: { origin: "http://localhost:20128" }, + }) + ); + const partialPortBody = await partialPortResponse.json(); + assert.equal( + new URL(partialPortBody.authUrl).searchParams.get("callback"), + "http://localhost:5959/callback" + ); +}); + +test("Command Code callback rejects disallowed origins and oversized bodies", async () => { + const disallowed = await callbackRoute.POST( + jsonRequest( + "http://localhost:20128/api/providers/command-code/auth/callback", + { apiKey: "secret", state: "x".repeat(32) }, + { origin: "https://evil.example" } + ) + ); + assert.equal(disallowed.status, 403); + assert.equal((await disallowed.json()).success, false); + assert.equal(disallowed.headers.get("access-control-allow-origin"), null); + + const tooLarge = await callbackRoute.POST( + new Request("http://localhost:20128/api/providers/command-code/auth/callback", { + method: "POST", + headers: { "content-type": "application/json", origin: "https://commandcode.ai" }, + body: JSON.stringify({ apiKey: "x".repeat(11 * 1024), state: "s".repeat(64) }), + }) + ); + assert.equal(tooLarge.status, 413); + assert.equal((await tooLarge.json()).success, false); + assert.equal(tooLarge.headers.get("access-control-allow-origin"), "https://commandcode.ai"); +}); diff --git a/tests/unit/command-code-executor.test.ts b/tests/unit/command-code-executor.test.ts new file mode 100644 index 0000000000..5c10b0bcd6 --- /dev/null +++ b/tests/unit/command-code-executor.test.ts @@ -0,0 +1,249 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-command-code-executor-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { REGISTRY, getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); +const { CommandCodeExecutor } = await import("../../open-sse/executors/commandCode.ts"); +const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const core = await import("../../src/lib/db/core.ts"); + +const originalFetch = globalThis.fetch; + +const PINNED_COMMAND_CODE_MODELS = [ + "claude-opus-4-7", + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-haiku-4-5-20251001", + "gpt-5.5", + "gpt-5.4", + "gpt-5.3-codex", + "gpt-5.4-mini", + "deepseek/deepseek-v4-pro", + "deepseek/deepseek-v4-flash", + "moonshotai/Kimi-K2.6", + "moonshotai/Kimi-K2.5", + "zai-org/GLM-5.1", + "zai-org/GLM-5", + "MiniMaxAI/MiniMax-M2.7", + "MiniMaxAI/MiniMax-M2.5", + "Qwen/Qwen3.6-Max-Preview", + "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: any) { + if (headers instanceof Headers) return Object.fromEntries(headers.entries()); + return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)])); +} + +function parseSsePayloads(sse: string) { + return sse + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => line.slice(6).trim()) + .filter((line) => line && line !== "[DONE]") + .map((line) => JSON.parse(line)); +} + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +test.after(() => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("Command Code provider catalog has pinned models and alias lookup", () => { + const entry = REGISTRY["command-code"]; + assert.ok(entry); + 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"); + assert.deepEqual( + entry.models.map((model) => model.id), + PINNED_COMMAND_CODE_MODELS + ); + assert.equal(getRegistryEntry("cmd"), entry); +}); + +test("getExecutor returns the specialized Command Code executor", () => { + assert.equal(hasSpecializedExecutor("command-code"), true); + assert.ok(getExecutor("command-code") instanceof CommandCodeExecutor); + assert.ok(getExecutor("cmd") instanceof CommandCodeExecutor); +}); + +test("Command Code executor posts wrapped body and required headers to alpha/generate", async () => { + const calls: any[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init }); + return commandCodeStream([{ type: "text-delta", text: "hello" }, { type: "finish" }]); + }; + + const executor = getExecutor("command-code"); + const { response, url, headers, transformedBody } = await executor.execute({ + model: "gpt-5.4-mini", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + stream: false, + messages: [ + { role: "system", content: "You are concise." }, + { role: "user", content: "Hi" }, + ], + tools: [{ type: "function", function: { name: "lookup", parameters: { type: "object" } } }], + max_tokens: 42, + }, + }); + + assert.equal(url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(calls[0].init.method, "POST"); + assert.equal(headers.Authorization, "Bearer cc_test_key"); + assert.equal(headers["x-command-code-version"], "0.24.1"); + assert.equal(headers["x-cli-environment"], "production"); + 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"); + + 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.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 json = await response.json(); + assert.equal(json.choices[0].message.content, "hello"); +}); + +test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () => { + globalThis.fetch = async () => + 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" }, + ]); + + const { response } = await getExecutor("command-code").execute({ + model: "gpt-5.4", + stream: true, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Hi" }] }, + }); + + 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(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"); +}); + +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 } + ); + + 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" }] }, + }); + + 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: 5, completion_tokens: 5, total_tokens: 10 }); +}); + +test("Command Code executor surfaces upstream and streamed 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, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Hi" }] }, + }); + 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 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({ + model: "gpt-5.4-mini", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Hi" }] }, + }); + }, /boom/); +}); diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index 215b636654..c5d0c6110a 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -1,8 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { validateProviderApiKey, validateClaudeCodeCompatibleProvider } = - await import("../../src/lib/providers/validation.ts"); +const { + validateProviderApiKey, + validateClaudeCodeCompatibleProvider, + validateCommandCodeProvider, +} = await import("../../src/lib/providers/validation.ts"); const originalFetch = globalThis.fetch; @@ -10,6 +13,13 @@ test.afterEach(() => { globalThis.fetch = originalFetch; }); +function toPlainHeaders(headers: any) { + if (headers instanceof Headers) return Object.fromEntries(headers.entries()); + return Object.fromEntries( + Object.entries(headers || {}).map(([key, value]) => [key, String(value)]) + ); +} + function metaAiSseText(content: string, streamingState = "DONE") { return `event: next data: ${JSON.stringify({ @@ -73,6 +83,28 @@ test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, Elev assert.equal(inworld.valid, true); }); +test("validateCommandCodeProvider ignores caller baseUrl and chatPath overrides", async () => { + globalThis.fetch = async (url, init = {}) => { + assert.equal(String(url), "https://api.commandcode.ai/alpha/generate"); + 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"); + return new Response(JSON.stringify({ ok: true }), { status: 200 }); + }; + + const result = await validateCommandCodeProvider({ + apiKey: "cc-key", + providerSpecificData: { + baseUrl: "https://evil.example/api", + chatPath: "/v1/chat/completions", + validationModelId: "command-code-validation-model", + }, + }); + + assert.equal(result.valid, true); +}); + test("specialty providers surface network failures and non-auth upstream failures", async () => { globalThis.fetch = async (url) => { const target = String(url); @@ -1876,3 +1908,63 @@ 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 () => { + const calls: any[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ + url: String(url), + method: init.method, + headers: toPlainHeaders(init.headers), + body: JSON.parse(String(init.body)), + }); + return new Response("", { status: 400 }); + }; + + const result = await validateCommandCodeProvider({ + apiKey: "cc_test_key", + providerSpecificData: { validationModelId: "gpt-5.4-mini" }, + }); + + assert.deepEqual(result, { valid: true, error: null }); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://api.commandcode.ai/alpha/generate"); + 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"], "0.24.1"); + assert.equal(calls[0].headers["x-cli-environment"], "production"); + 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, "omniroute-validation"); + assert.equal(calls[0].body.permissionMode, "standard"); + 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); +}); + +for (const status of [400, 422, 429]) { + test(`validateCommandCodeProvider accepts ${status} as direct validator auth success`, async () => { + globalThis.fetch = async () => new Response("", { status }); + assert.deepEqual(await validateCommandCodeProvider({ apiKey: "cc_test_key" }), { + valid: true, + error: null, + }); + }); +} + +test("validateCommandCodeProvider rejects auth failures and provider outages", async () => { + globalThis.fetch = async () => new Response("unauthorized", { status: 401 }); + assert.deepEqual(await validateCommandCodeProvider({ apiKey: "bad" }), { + valid: false, + error: "Invalid API key", + }); + + globalThis.fetch = async () => new Response("server down", { status: 500 }); + assert.deepEqual(await validateCommandCodeProvider({ apiKey: "cc_test_key" }), { + valid: false, + error: "Provider unavailable (500)", + }); +}); diff --git a/tests/unit/responses-handler.test.ts b/tests/unit/responses-handler.test.ts index f76a4d4881..450ca31099 100644 --- a/tests/unit/responses-handler.test.ts +++ b/tests/unit/responses-handler.test.ts @@ -224,6 +224,45 @@ test("handleResponsesCore transforms upstream OpenAI SSE into Responses API SSE" assert.match(sse, /data: \[DONE\]/); }); +test("handleResponsesCore transforms Command Code executor SSE through Responses shim", async () => { + const { call, result } = await invokeResponsesCore({ + provider: "command-code", + model: "gpt-5.4-mini", + credentials: { apiKey: "cc_test_key", providerSpecificData: {} }, + body: { + model: "gpt-5.4-mini", + input: "hello command code", + }, + responseFactory() { + 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" } } + ); + }, + }); + + assert.equal(result.success, true); + assert.equal(call.url, "https://api.commandcode.ai/alpha/generate"); + assert.equal(call.headers.Authorization, "Bearer cc_test_key"); + assert.equal(call.headers["x-command-code-version"], "0.24.1"); + assert.equal(call.body.params.model, "gpt-5.4-mini"); + assert.equal(call.body.params.stream, true); + + const sse = await result.response.text(); + assert.match(sse, /event: response\.created/); + assert.match(sse, /event: response\.output_text\.delta/); + assert.match(sse, /command/); + assert.match(sse, /event: response\.completed/); + assert.match(sse, /data: \[DONE\]/); +}); + test("handleResponsesCore propagates upstream failures from chatCore unchanged", async () => { const { result } = await invokeResponsesCore({ body: {