From b72a443bd38c37fc498773ed64f269edbb3bd047 Mon Sep 17 00:00:00 2001 From: decolua Date: Thu, 7 May 2026 23:01:33 +0700 Subject: [PATCH] feat: add CommandCode provider support --- open-sse/config/providerModels.js | 13 ++ open-sse/config/providers.js | 8 + open-sse/executors/commandcode.js | 88 ++++++++ open-sse/executors/index.js | 3 + open-sse/handlers/chatCore.js | 2 +- open-sse/services/model.js | 2 + open-sse/translator/formats.js | 3 +- open-sse/translator/index.js | 2 + .../request/openai-to-commandcode.js | 81 ++++++++ .../response/commandcode-to-openai.js | 194 ++++++++++++++++++ public/providers/commandcode.png | Bin 0 -> 6585 bytes src/shared/constants/providers.js | 1 + 12 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 open-sse/executors/commandcode.js create mode 100644 open-sse/translator/request/openai-to-commandcode.js create mode 100644 open-sse/translator/response/commandcode-to-openai.js create mode 100644 public/providers/commandcode.png diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index a0888ed7..5b91396e 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -408,6 +408,19 @@ export const PROVIDER_MODELS = { { id: "deepseek-chat", name: "DeepSeek V3.2 Chat" }, { id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" }, ], + commandcode: [ + { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" }, + { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash" }, + { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6" }, + { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" }, + { id: "zai-org/GLM-5.1", name: "GLM 5.1" }, + { id: "zai-org/GLM-5", name: "GLM 5" }, + { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7" }, + { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" }, + { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview" }, + { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" }, + { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" }, + ], groq: [ { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" }, { id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick" }, diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index 323291db..4c844c5f 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -251,6 +251,14 @@ export const PROVIDERS = { baseUrl: "https://api.deepseek.com/chat/completions", format: "openai" }, + commandcode: { + baseUrl: "https://api.commandcode.ai/alpha/generate", + format: "commandcode", + headers: { + "x-command-code-version": "0.25.7", + "x-cli-environment": "cli" + } + }, groq: { baseUrl: "https://api.groq.com/openai/v1/chat/completions", format: "openai" diff --git a/open-sse/executors/commandcode.js b/open-sse/executors/commandcode.js new file mode 100644 index 00000000..e0c10e88 --- /dev/null +++ b/open-sse/executors/commandcode.js @@ -0,0 +1,88 @@ +import { randomUUID } from "crypto"; +import { BaseExecutor } from "./base.js"; +import { PROVIDERS } from "../config/providers.js"; +import { convertCommandCodeToOpenAI } from "../translator/response/commandcode-to-openai.js"; + +/** + * CommandCodeExecutor — talks to https://api.commandcode.ai/alpha/generate + * + * Auth: Bearer API key (stored as the connection's apiKey). + * Adds the per-request `x-session-id` header expected by CommandCode upstream. + * + * Upstream returns AI SDK v5 NDJSON (one JSON event per line, no `data:` prefix). + * We translate each event to an OpenAI chat.completion.chunk and emit it as SSE so + * both the streaming and non-streaming (forced SSE → JSON) downstream handlers in + * 9router can consume it without further format translation. + */ +export class CommandCodeExecutor extends BaseExecutor { + constructor() { + super("commandcode", PROVIDERS.commandcode); + } + + buildHeaders(credentials, stream = true) { + const headers = { + "Content-Type": "application/json", + ...(this.config.headers || {}), + "x-session-id": randomUUID(), + }; + + const token = credentials?.apiKey || credentials?.accessToken; + if (token) headers["Authorization"] = `Bearer ${token}`; + + if (stream) headers["Accept"] = "text/event-stream"; + return headers; + } + + async execute(opts) { + const result = await super.execute(opts); + if (!result?.response?.ok || !result.response.body) return result; + result.response = wrapNdjsonAsOpenAISse(result.response, opts.model); + return result; + } +} + +function wrapNdjsonAsOpenAISse(originalResponse, model) { + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + let buffer = ""; + const state = { model }; + + const emitChunks = (chunks, controller) => { + if (!chunks) return; + const list = Array.isArray(chunks) ? chunks : [chunks]; + for (const c of list) { + if (c == null) continue; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(c)}\n\n`)); + } + }; + + const transform = new TransformStream({ + transform(chunk, controller) { + buffer += decoder.decode(chunk, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + // Translate AI SDK v5 NDJSON line to one or more OpenAI chunks + emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller); + } + }, + flush(controller) { + const trimmed = buffer.trim(); + if (trimmed) { + emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller); + } + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + }, + }); + + const newBody = originalResponse.body.pipeThrough(transform); + return new Response(newBody, { + status: originalResponse.status, + statusText: originalResponse.statusText, + headers: originalResponse.headers, + }); +} + +export default CommandCodeExecutor; diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index 9479f4a5..78372cbf 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -14,6 +14,7 @@ import { OpenCodeGoExecutor } from "./opencode-go.js"; import { GrokWebExecutor } from "./grok-web.js"; import { PerplexityWebExecutor } from "./perplexity-web.js"; import { OllamaLocalExecutor } from "./ollama-local.js"; +import { CommandCodeExecutor } from "./commandcode.js"; import { DefaultExecutor } from "./default.js"; const executors = { @@ -35,6 +36,7 @@ const executors = { "grok-web": new GrokWebExecutor(), "perplexity-web": new PerplexityWebExecutor(), "ollama-local": new OllamaLocalExecutor(), + commandcode: new CommandCodeExecutor(), }; const defaultCache = new Map(); @@ -67,3 +69,4 @@ export { OpenCodeGoExecutor } from "./opencode-go.js"; export { GrokWebExecutor } from "./grok-web.js"; export { PerplexityWebExecutor } from "./perplexity-web.js"; export { OllamaLocalExecutor } from "./ollama-local.js"; +export { CommandCodeExecutor } from "./commandcode.js"; diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 338e92eb..ca59cb99 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -56,7 +56,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred } const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI; - const providerRequiresStreaming = provider === "openai" || provider === "codex"; + const providerRequiresStreaming = provider === "openai" || provider === "codex" || provider === "commandcode"; let stream = providerRequiresStreaming ? true : (body.stream !== false); // Check client Accept header preference for non-streaming requests diff --git a/open-sse/services/model.js b/open-sse/services/model.js index c127fe7f..234ed563 100644 --- a/open-sse/services/model.js +++ b/open-sse/services/model.js @@ -27,6 +27,8 @@ const ALIAS_TO_PROVIDER_ID = { "minimax-cn": "minimax-cn", ds: "deepseek", deepseek: "deepseek", + cmc: "commandcode", + commandcode: "commandcode", groq: "groq", xai: "xai", mistral: "mistral", diff --git a/open-sse/translator/formats.js b/open-sse/translator/formats.js index f5594183..89367d00 100644 --- a/open-sse/translator/formats.js +++ b/open-sse/translator/formats.js @@ -11,7 +11,8 @@ export const FORMATS = { ANTIGRAVITY: "antigravity", KIRO: "kiro", CURSOR: "cursor", - OLLAMA: "ollama" + OLLAMA: "ollama", + COMMANDCODE: "commandcode" }; /** diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index a94a00ca..d581bd7e 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -40,6 +40,7 @@ function ensureInitialized() { require("./request/openai-to-kiro.js"); require("./request/openai-to-cursor.js"); require("./request/openai-to-ollama.js"); + require("./request/openai-to-commandcode.js"); // Response translators require("./response/claude-to-openai.js"); @@ -50,6 +51,7 @@ function ensureInitialized() { require("./response/kiro-to-openai.js"); require("./response/cursor-to-openai.js"); require("./response/ollama-to-openai.js"); + require("./response/commandcode-to-openai.js"); } // Strip specific content types from messages (explicit opt-in via strip[] in PROVIDER_MODELS) diff --git a/open-sse/translator/request/openai-to-commandcode.js b/open-sse/translator/request/openai-to-commandcode.js new file mode 100644 index 00000000..6dd6e266 --- /dev/null +++ b/open-sse/translator/request/openai-to-commandcode.js @@ -0,0 +1,81 @@ +/** + * OpenAI → CommandCode request translator + * + * CommandCode endpoint expects an envelope: + * { threadId, memory, config, params: { model, messages, stream, max_tokens, temperature, tools? } } + * where `params.messages` are Anthropic-style content blocks ([{type:"text", text}, ...]). + * + * The model id received here is the upstream id (e.g. "deepseek/deepseek-v4-pro") thanks to the + * `provider/model` registration in providerModels.js. + */ +import { register } from "../index.js"; +import { FORMATS } from "../formats.js"; +import { randomUUID } from "crypto"; + +function toContentBlocks(content) { + if (content == null) return [{ type: "text", text: "" }]; + if (typeof content === "string") return [{ type: "text", text: content }]; + if (Array.isArray(content)) { + const blocks = []; + for (const part of content) { + if (typeof part === "string") { + blocks.push({ type: "text", text: part }); + } else if (part && typeof part === "object") { + if (part.type === "text" && typeof part.text === "string") { + blocks.push({ type: "text", text: part.text }); + } else if (part.type === "image_url" || part.type === "image") { + // CommandCode currently rejects multimodal blocks via this gateway; + // collapse to a textual placeholder so the request still validates. + blocks.push({ type: "text", text: "[image omitted]" }); + } else if (typeof part.text === "string") { + blocks.push({ type: "text", text: part.text }); + } + } + } + return blocks.length ? blocks : [{ type: "text", text: "" }]; + } + return [{ type: "text", text: String(content) }]; +} + +function convertMessages(messages = []) { + return messages.map((m) => { + const role = m.role === "tool" ? "user" : (m.role || "user"); + return { role, content: toContentBlocks(m.content) }; + }); +} + +export function openaiToCommandCode(model, body, stream /* , credentials */) { + const params = { + model, + messages: convertMessages(body.messages), + stream: stream !== false, + max_tokens: body.max_tokens ?? body.max_output_tokens ?? 64000, + temperature: body.temperature ?? 0.3, + }; + + if (Array.isArray(body.tools) && body.tools.length > 0) { + params.tools = body.tools; + } + if (body.top_p != null) params.top_p = body.top_p; + + const today = new Date().toISOString().slice(0, 10); + + return { + threadId: randomUUID(), + memory: "", + config: { + workingDir: process.cwd(), + date: today, + environment: process.platform, + structure: [], + isGitRepo: false, + currentBranch: "", + mainBranch: "", + gitStatus: "", + recentCommits: [], + }, + params, + }; +} + +register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCode, null); diff --git a/open-sse/translator/response/commandcode-to-openai.js b/open-sse/translator/response/commandcode-to-openai.js new file mode 100644 index 00000000..1290b9ba --- /dev/null +++ b/open-sse/translator/response/commandcode-to-openai.js @@ -0,0 +1,194 @@ +/** + * CommandCode → OpenAI response translator + * + * CommandCode upstream emits NDJSON-style AI SDK v5 stream events: + * {"type":"start"} {"type":"start-step", ...} + * {"type":"reasoning-start","id":"..."} {"type":"reasoning-delta","text":"..."} + * {"type":"text-start","id":"..."} {"type":"text-delta","text":"..."} + * {"type":"tool-input-start","toolCallId","toolName"} + * {"type":"tool-input-delta","toolCallId","inputTextDelta"} + * {"type":"tool-call","toolCallId","toolName","input"} + * {"type":"finish-step","finishReason","usage": {...}, ...} + * {"type":"finish",...} + * + * Each upstream "event" arrives as one JSON object per line — we receive it as a string chunk + * already split per line by the upstream SSE/JSON-line reader in 9router. + */ +import { register } from "../index.js"; +import { FORMATS } from "../formats.js"; + +function ensureState(state, model) { + if (!state.responseId) { + state.responseId = `chatcmpl-${Date.now()}`; + state.created = Math.floor(Date.now() / 1000); + state.model = state.model || model || "commandcode"; + state.chunkIndex = 0; + state.toolIndex = 0; + state.toolIndexById = new Map(); + state.openTools = new Set(); + state.openText = false; + state.finishReason = null; + state.usage = null; + } +} + +function makeChunk(state, delta, finishReason = null) { + return { + id: state.responseId, + object: "chat.completion.chunk", + created: state.created, + model: state.model, + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +function mapFinishReason(reason) { + switch (reason) { + case "stop": return "stop"; + case "length": return "length"; + case "tool-calls": + case "tool_use": return "tool_calls"; + case "content-filter": return "content_filter"; + case "error": return "stop"; + default: return reason || "stop"; + } +} + +export function convertCommandCodeToOpenAI(chunk, state) { + if (!chunk) return null; + + // Already-OpenAI chunk: pass through + if (chunk && typeof chunk === "object" && chunk.object === "chat.completion.chunk") { + return chunk; + } + + // Parse string lines coming out of upstream + let event = chunk; + if (typeof chunk === "string") { + const line = chunk.trim(); + if (!line) return null; + // Tolerate raw "data: {...}" framing if the upstream wrapper inserts it + const json = line.startsWith("data:") ? line.slice(5).trim() : line; + if (!json || json === "[DONE]") return null; + try { + event = JSON.parse(json); + } catch { + return null; + } + } + + if (!event || typeof event !== "object" || !event.type) return null; + + ensureState(state, event.model); + const out = []; + + switch (event.type) { + case "text-delta": { + const text = event.text || event.delta || ""; + if (!text) break; + const delta = state.chunkIndex === 0 ? { role: "assistant", content: text } : { content: text }; + state.chunkIndex++; + state.openText = true; + out.push(makeChunk(state, delta)); + break; + } + case "reasoning-delta": { + const text = event.text || ""; + if (!text) break; + // Map reasoning to OpenAI "reasoning_content" field (used by deepseek-reasoner-style clients). + const delta = state.chunkIndex === 0 + ? { role: "assistant", reasoning_content: text } + : { reasoning_content: text }; + state.chunkIndex++; + out.push(makeChunk(state, delta)); + break; + } + case "tool-input-start": { + const id = event.toolCallId || `call_${Date.now()}_${state.toolIndex}`; + let idx = state.toolIndexById.get(id); + if (idx == null) { + idx = state.toolIndex++; + state.toolIndexById.set(id, idx); + } + state.openTools.add(id); + const delta = { + ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + tool_calls: [{ + index: idx, + id, + type: "function", + function: { name: event.toolName || "", arguments: "" }, + }], + }; + state.chunkIndex++; + out.push(makeChunk(state, delta)); + break; + } + case "tool-input-delta": { + const id = event.toolCallId; + const idx = state.toolIndexById.get(id); + if (idx == null) break; + const delta = { + tool_calls: [{ + index: idx, + function: { arguments: event.inputTextDelta || event.delta || "" }, + }], + }; + out.push(makeChunk(state, delta)); + break; + } + case "tool-call": { + // Final consolidated tool call — only emit if we never saw tool-input-* deltas. + const id = event.toolCallId; + if (state.toolIndexById.has(id)) break; + const idx = state.toolIndex++; + state.toolIndexById.set(id, idx); + const argsStr = typeof event.input === "string" ? event.input : JSON.stringify(event.input ?? {}); + const delta = { + ...(state.chunkIndex === 0 ? { role: "assistant" } : {}), + tool_calls: [{ + index: idx, + id, + type: "function", + function: { name: event.toolName || "", arguments: argsStr }, + }], + }; + state.chunkIndex++; + out.push(makeChunk(state, delta)); + break; + } + case "finish-step": { + state.finishReason = mapFinishReason(event.finishReason); + if (event.usage) state.usage = event.usage; + break; + } + case "finish": { + const finishReason = state.finishReason || mapFinishReason(event.finishReason || "stop"); + const finalChunk = makeChunk(state, {}, finishReason); + const totalUsage = event.totalUsage || state.usage; + if (totalUsage) { + finalChunk.usage = { + prompt_tokens: totalUsage.inputTokens ?? 0, + completion_tokens: totalUsage.outputTokens ?? 0, + total_tokens: totalUsage.totalTokens ?? ((totalUsage.inputTokens ?? 0) + (totalUsage.outputTokens ?? 0)), + }; + } + out.push(finalChunk); + break; + } + case "error": { + state.finishReason = "stop"; + out.push(makeChunk(state, { content: `\n\n[CommandCode error: ${event.error || event.message || "unknown"}]` })); + out.push(makeChunk(state, {}, "stop")); + break; + } + // Silently ignore: start, start-step, reasoning-start, reasoning-end, text-start, text-end, + // provider-metadata, message-metadata, etc. They carry no client-visible content. + default: + break; + } + + return out.length ? out : null; +} + +register(FORMATS.COMMANDCODE, FORMATS.OPENAI, null, convertCommandCodeToOpenAI); diff --git a/public/providers/commandcode.png b/public/providers/commandcode.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7c8c99b9b72c21aced2b4a1435d32552b228dc GIT binary patch literal 6585 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 BMFs!> literal 0 HcmV?d00001 diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js index 4423ad94..e7285d4b 100644 --- a/src/shared/constants/providers.js +++ b/src/shared/constants/providers.js @@ -69,6 +69,7 @@ export const APIKEY_PROVIDERS = { azure: { id: "azure", alias: "azure", name: "Azure OpenAI", icon: "cloud", color: "#0078D4", textIcon: "AZ", website: "https://azure.microsoft.com/en-us/products/ai-services/openai-service", notice: { apiKeyUrl: "https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI" }, hasProviderSpecificData: true }, deepseek: { id: "deepseek", alias: "ds", name: "DeepSeek", icon: "bolt", color: "#4D6BFE", textIcon: "DS", website: "https://deepseek.com", notice: { apiKeyUrl: "https://platform.deepseek.com/api_keys" } }, + commandcode: { id: "commandcode", alias: "cmc", name: "Command Code", icon: "smart_toy", color: "#000000", textIcon: "CC", website: "https://commandcode.ai", notice: { text: "Use your CommandCode CLI API key (starts with user_...) from ~/.commandcode/auth.json or commandcode.ai/studio.", apiKeyUrl: "https://commandcode.ai/studio" } }, groq: { id: "groq", alias: "groq", name: "Groq", icon: "speed", color: "#F55036", textIcon: "GQ", website: "https://groq.com", notice: { apiKeyUrl: "https://console.groq.com/keys" }, serviceKinds: ["llm", "imageToText", "stt"], sttConfig: { baseUrl: "https://api.groq.com/openai/v1/audio/transcriptions", authType: "apikey", authHeader: "bearer", format: "openai", models: [{ id: "whisper-large-v3", name: "Whisper Large v3" }, { id: "whisper-large-v3-turbo", name: "Whisper Large v3 Turbo" }, { id: "distil-whisper-large-v3-en", name: "Distil Whisper Large v3 EN" }] } }, xai: { id: "xai", alias: "xai", name: "xAI (Grok)", icon: "auto_awesome", color: "#1DA1F2", textIcon: "XA", website: "https://x.ai", notice: { apiKeyUrl: "https://console.x.ai" }, serviceKinds: ["llm", "imageToText", "webSearch"], searchViaChat: { defaultModel: "grok-4.20-reasoning", pricingUrl: "https://x.ai/api#pricing" } }, mistral: { id: "mistral", alias: "mistral", name: "Mistral", icon: "air", color: "#FF7000", textIcon: "MI", website: "https://mistral.ai", notice: { apiKeyUrl: "https://console.mistral.ai/api-keys" }, serviceKinds: ["llm", "imageToText", "embedding"], embeddingConfig: { baseUrl: "https://api.mistral.ai/v1/embeddings", authType: "apikey", authHeader: "bearer", models: [{ id: "mistral-embed", name: "Mistral Embed", dimensions: 1024 }] } },