chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,716 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { isVisionModelId } from "@/shared/constants/visionModels";
|
||||
import { REGISTRY } from "../config/providerRegistry.ts";
|
||||
import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./base.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export const COMMAND_CODE_VERSION = process.env.COMMAND_CODE_VERSION?.trim() || "0.33.2";
|
||||
// Hard server-side ceiling enforced by Command Code's /alpha/generate endpoint:
|
||||
// any request with params.max_tokens > 200_000 is rejected with a 400
|
||||
// "Too big: expected number to be <=200000 at params.max_tokens". We only use
|
||||
// this to clamp a CLIENT-SUPPLIED max_tokens down to a value the endpoint will
|
||||
// accept; we never fabricate this number for requests that omit the field (see
|
||||
// clampMaxTokens / buildCommandCodeBody).
|
||||
const MAX_COMMAND_CODE_TOKENS = 200_000;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function asRecordArray(value: unknown): JsonRecord[] {
|
||||
return Array.isArray(value) ? value.filter(isRecord) : [];
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function recordOrEmpty(value: unknown): JsonRecord {
|
||||
if (isRecord(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (isRecord(parsed)) return parsed;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[commandCode] tool arg parse failed:",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function normalizeContentText(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
return asRecordArray(content)
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => stringValue(part.text) || "")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Model id patterns for Command Code models that have `text, vision`
|
||||
* capability per the official CC model registry, but are NOT caught
|
||||
* by the shared {@link isVisionModelId} heuristic. Kept as a local
|
||||
* set because these are CC-specific model IDs (vendor-prefix shapes
|
||||
* like "moonshotai/Kimi-K2.6" or CC aliases like "gpt-5.4-mini").
|
||||
*
|
||||
* Source: Command Code /alpha/generate model registry (docs).
|
||||
*/
|
||||
const CC_VISION_MODEL_PATTERNS: readonly RegExp[] = [
|
||||
// Open Source
|
||||
/kimi-k2/i, // moonshotai/Kimi-K2.6, Kimi-K2.7-Code, Kimi-K2.5
|
||||
/qwen3\.\d/i, // Qwen/Qwen3.6-Plus, Qwen/Qwen3.7-Plus
|
||||
/step-?3/i, // stepfun/Step-3.7-Flash
|
||||
// Anthropic
|
||||
/claude-fable/i, // claude-fable-5 (not covered by claude-opus/sonnet/haiku-4)
|
||||
// OpenAI
|
||||
/gpt-5/i, // gpt-5.5, gpt-5.4, gpt-5.3-codex, gpt-5.4-mini
|
||||
// Sakana
|
||||
/fugu/i, // sakana/fugu-ultra
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether a model id routed through the Command Code executor is
|
||||
* vision-capable. Checks Mimo-specific rules first, then CC-specific
|
||||
* patterns, then falls through to the shared {@link isVisionModelId}
|
||||
* heuristic (which covers minimax-m3, claude-3/4 families, gemini,
|
||||
* gpt-4o/4.1, mistral-medium-3, and general "-vision" / "multimodal").
|
||||
*/
|
||||
function isCommandCodeVisionModel(model?: string | null): boolean {
|
||||
if (!model) return false;
|
||||
// mimo-v2.5-pro is text-only — exclude before any positive check
|
||||
if (/(?:^|\/)mimo-v2\.5-pro$/i.test(model)) return false;
|
||||
// Only mimo-v2.5 and mimo-v2-omni accept images per Xiaomi vendor docs
|
||||
if (/(?:^|\/)mimo-v2\.5$/i.test(model)) return true;
|
||||
if (/(?:^|\/)mimo-v2-omni$/i.test(model)) return true;
|
||||
// CC-specific patterns: Kimi K2, Qwen 3.x, Stepfun, Claude Fable,
|
||||
// GPT-5, Sakana Fugu — not covered by the shared heuristic
|
||||
if (CC_VISION_MODEL_PATTERNS.some((pattern) => pattern.test(model))) return true;
|
||||
// Fall through: minimax-m3, claude-3/4, gemini-2/3, gpt-4o, -vision, multimodal
|
||||
return isVisionModelId(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the image URL from an OpenAI-compatible or Command Code
|
||||
* content part, returning undefined for non-image parts.
|
||||
*
|
||||
* OpenAI-compatible: { type: "image_url", image_url: { url: "..." } }
|
||||
* Command Code CLI: { type: "image", image: "..." }
|
||||
*/
|
||||
function extractImageUrl(part: JsonRecord): string | undefined {
|
||||
if (part.type === "image") return stringValue(part.image);
|
||||
if (part.type === "image_url") {
|
||||
if (isRecord(part.image_url)) return stringValue(part.image_url.url);
|
||||
return stringValue(part.image_url);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an OpenAI-format content array to Command Code's internal
|
||||
* CLI format. For vision-capable models (MiniMax M3, MiMo v2.5, etc.)
|
||||
* this also preserves image parts alongside text.
|
||||
*/
|
||||
function convertUserContentParts(content: unknown, isVisionModel: boolean): string | unknown[] {
|
||||
// For non-vision models or string content, extract text only.
|
||||
if (!isVisionModel || typeof content === "string") {
|
||||
return normalizeContentText(content);
|
||||
}
|
||||
|
||||
const parts: unknown[] = [];
|
||||
for (const part of asRecordArray(content)) {
|
||||
if (part.type === "text") {
|
||||
const text = stringValue(part.text);
|
||||
if (text) parts.push({ type: "text", text });
|
||||
continue;
|
||||
}
|
||||
const imgUrl = extractImageUrl(part);
|
||||
if (imgUrl) {
|
||||
parts.push({ type: "image", image: imgUrl });
|
||||
continue;
|
||||
}
|
||||
// Always drop tool_use / tool_result / thinking parts from user
|
||||
// messages (Command Code doesn't accept them for role:"user").
|
||||
}
|
||||
|
||||
// When every part was stripped, fall back to empty text so the
|
||||
// message is still valid JSON (Command Code rejects empty content).
|
||||
if (parts.length === 0) parts.push({ type: "text", text: "" });
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function convertTools(tools: unknown): 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<string> {
|
||||
const callIds = new Set<string>();
|
||||
const resultIds = new Set<string>();
|
||||
|
||||
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,
|
||||
model?: string | null
|
||||
): { system: string; messages: unknown[] } {
|
||||
const source = asRecordArray(messages);
|
||||
const pairedToolCallIds = completeToolCallIds(source);
|
||||
const out: unknown[] = [];
|
||||
const system: string[] = [];
|
||||
const isVision = isCommandCodeVisionModel(model);
|
||||
|
||||
for (const message of source) {
|
||||
const role = stringValue(message.role);
|
||||
if (role === "system" || role === "developer") {
|
||||
const text = normalizeContentText(message.content);
|
||||
if (text) system.push(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "user") {
|
||||
out.push({ role: "user", content: convertUserContentParts(message.content, isVision) });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "assistant") {
|
||||
const parts: unknown[] = [];
|
||||
const text = normalizeContentText(message.content);
|
||||
if (text) parts.push({ type: "text", text });
|
||||
|
||||
for (const call of asRecordArray(message.tool_calls)) {
|
||||
const id = stringValue(call.id) || "";
|
||||
if (!id || !pairedToolCallIds.has(id)) continue;
|
||||
const fn = isRecord(call.function) ? call.function : {};
|
||||
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 };
|
||||
}
|
||||
|
||||
// Clamp a client-supplied max_tokens to the endpoint ceiling, mirroring the
|
||||
// provider-driven clamp in antigravity.ts: we only intervene when the value is
|
||||
// present, positive AND would otherwise be rejected (> 200_000). A valid value
|
||||
// is returned floored; anything absent, non-numeric or non-positive returns
|
||||
// undefined so the caller can OMIT the field entirely and let Command Code's
|
||||
// upstream apply the model's own native default (rather than us inventing a
|
||||
// number). A non-positive value such as Zoo Code's max_tokens:-1 ("let the
|
||||
// server choose") must be omitted, NOT forced to 1 — the old Math.max(1,...)
|
||||
// truncated output to a single token (#5166).
|
||||
function clampMaxTokens(value: unknown): number | undefined {
|
||||
const numeric = numberValue(value);
|
||||
if (numeric === undefined || numeric <= 0) return undefined;
|
||||
return Math.min(Math.floor(numeric), MAX_COMMAND_CODE_TOKENS);
|
||||
}
|
||||
|
||||
// Reasoning/thinking fields that payload rules or clients may inject and that
|
||||
// CommandCode's upstream accepts inside `params`. Without this pass-through,
|
||||
// payload-rule overrides on these fields are silently dropped (#2986 follow-up).
|
||||
const COMMAND_CODE_PASSTHROUGH_FIELDS = [
|
||||
"reasoning_effort",
|
||||
"reasoning",
|
||||
"thinking",
|
||||
"effort",
|
||||
"output_config",
|
||||
"extra_body",
|
||||
] as const;
|
||||
|
||||
function buildCommandCodeBody(model: string, body: unknown, stream = false): JsonRecord {
|
||||
const input = isRecord(body) ? body : {};
|
||||
|
||||
// Payload rules may rewrite `body.model` (e.g. deepseek-v4-pro-max →
|
||||
// deepseek/deepseek-v4-pro for the command-code provider). Prefer the
|
||||
// rewritten value if present; fall back to the resolved combo model arg.
|
||||
const resolvedModel =
|
||||
typeof input.model === "string" && input.model.trim().length > 0 ? input.model : model;
|
||||
|
||||
const converted = convertMessages(input.messages, resolvedModel);
|
||||
const explicitSystem = typeof input.system === "string" ? input.system : "";
|
||||
const system = [converted.system, explicitSystem].filter(Boolean).join("\n\n");
|
||||
|
||||
const params: JsonRecord = {
|
||||
model: resolvedModel,
|
||||
messages: converted.messages,
|
||||
tools: convertTools(input.tools),
|
||||
system,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
// Only forward max_tokens when the client actually supplied one. Omitting it
|
||||
// lets Command Code's upstream apply the model's own native default, so we
|
||||
// never invent a value (the old behavior, which sent the wrong number and got
|
||||
// DeepSeek V4 rejected with "Too big: expected number to be <=200000"). When
|
||||
// present, it is clamped to the endpoint ceiling so an oversized client value
|
||||
// degrades gracefully instead of 400ing.
|
||||
const maxTokens = clampMaxTokens(input.max_tokens ?? input.max_completion_tokens);
|
||||
if (maxTokens !== undefined) {
|
||||
params.max_tokens = maxTokens;
|
||||
}
|
||||
|
||||
for (const field of COMMAND_CODE_PASSTHROUGH_FIELDS) {
|
||||
const value = input[field];
|
||||
if (value !== undefined && value !== null) {
|
||||
params[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
workingDir: "/workspace",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
environment: "external",
|
||||
structure: [],
|
||||
isGitRepo: false,
|
||||
currentBranch: "",
|
||||
mainBranch: "",
|
||||
gitStatus: "",
|
||||
recentCommits: [],
|
||||
},
|
||||
memory: "",
|
||||
taste: "",
|
||||
skills: "",
|
||||
permissionMode: "standard",
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
function parseStreamLine(line: string): unknown | undefined {
|
||||
let trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:")) return undefined;
|
||||
if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim();
|
||||
if (!trimmed || trimmed === "[DONE]") return undefined;
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[commandCode] stream line parse failed:",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function mapFinishReason(reason: unknown): "stop" | "length" | "tool_calls" {
|
||||
if (reason === "tool-calls" || reason === "tool_calls" || reason === "toolUse")
|
||||
return "tool_calls";
|
||||
if (
|
||||
reason === "length" ||
|
||||
reason === "max_tokens" ||
|
||||
reason === "max-tokens" ||
|
||||
reason === "max_output_tokens"
|
||||
) {
|
||||
return "length";
|
||||
}
|
||||
return "stop";
|
||||
}
|
||||
|
||||
function chatCompletionChunk(
|
||||
id: string,
|
||||
model: string,
|
||||
delta: JsonRecord,
|
||||
finishReason: unknown = null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
||||
};
|
||||
}
|
||||
|
||||
function sse(data: unknown): Uint8Array {
|
||||
return encoder.encode(`data: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
type AggregateState = {
|
||||
content: string;
|
||||
reasoning: string;
|
||||
toolCalls: JsonRecord[];
|
||||
finishReason: "stop" | "length" | "tool_calls";
|
||||
usage: JsonRecord | null;
|
||||
};
|
||||
|
||||
function 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<Uint8Array>({
|
||||
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 (error) {
|
||||
console.warn(
|
||||
"[commandCode] reader releaseLock failed:",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pump();
|
||||
},
|
||||
cancel() {
|
||||
closed = true;
|
||||
return reader?.cancel();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
|
||||
});
|
||||
}
|
||||
|
||||
async function createJsonResponse(
|
||||
upstream: Response,
|
||||
model: string,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<Response> {
|
||||
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 (error) {
|
||||
console.warn(
|
||||
"[commandCode] reader cancel failed:",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[commandCode] reader releaseLock failed:",
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const message: JsonRecord = { role: "assistant", content: state.content };
|
||||
if (state.reasoning) message.reasoning_content = state.reasoning;
|
||||
if (state.toolCalls.length > 0) message.tool_calls = state.toolCalls;
|
||||
|
||||
const payload: JsonRecord = {
|
||||
id: `chatcmpl-${randomUUID()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [{ index: 0, message, finish_reason: state.finishReason }],
|
||||
};
|
||||
const usage = usageFromCommandCode(state.usage);
|
||||
if (usage) payload.usage = usage;
|
||||
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
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<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"x-command-code-version": COMMAND_CODE_VERSION,
|
||||
"x-cli-environment": "external",
|
||||
"x-project-slug": "pi-cc",
|
||||
"x-taste-learning": "false",
|
||||
"x-co-flag": "false",
|
||||
"x-session-id": randomUUID(),
|
||||
};
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
||||
|
||||
const transformedBody = buildCommandCodeBody(model, body, stream);
|
||||
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(() => {
|
||||
console.warn("[commandCode] upstream text failed");
|
||||
return "";
|
||||
});
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user