mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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.
This commit is contained in:
@@ -156,6 +156,135 @@ const KIMI_CODING_SHARED = {
|
|||||||
const buildModels = (ids: readonly string[]): RegistryModel[] =>
|
const buildModels = (ids: readonly string[]): RegistryModel[] =>
|
||||||
ids.map((id) => ({ id, name: id }));
|
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_CONTEXT_LENGTH = 1050000;
|
||||||
const GPT_5_5_CODEX_CAPABILITIES = {
|
const GPT_5_5_CODEX_CAPABILITIES = {
|
||||||
targetFormat: "openai-responses",
|
targetFormat: "openai-responses",
|
||||||
@@ -991,6 +1120,20 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
|||||||
passthroughModels: true,
|
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: {
|
openrouter: {
|
||||||
id: "openrouter",
|
id: "openrouter",
|
||||||
alias: "openrouter",
|
alias: "openrouter",
|
||||||
|
|||||||
545
open-sse/executors/commandCode.ts
Normal file
545
open-sse/executors/commandCode.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
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<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): { 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<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 {
|
||||||
|
// 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<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 {
|
||||||
|
// 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<string, string> = {
|
||||||
|
"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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { ChatGptWebExecutor } from "./chatgpt-web.ts";
|
|||||||
import { BlackboxWebExecutor } from "./blackbox-web.ts";
|
import { BlackboxWebExecutor } from "./blackbox-web.ts";
|
||||||
import { MuseSparkWebExecutor } from "./muse-spark-web.ts";
|
import { MuseSparkWebExecutor } from "./muse-spark-web.ts";
|
||||||
import { AzureOpenAIExecutor } from "./azure-openai.ts";
|
import { AzureOpenAIExecutor } from "./azure-openai.ts";
|
||||||
|
import { CommandCodeExecutor } from "./commandCode.ts";
|
||||||
import { GitlabExecutor } from "./gitlab.ts";
|
import { GitlabExecutor } from "./gitlab.ts";
|
||||||
import { NlpCloudExecutor } from "./nlpcloud.ts";
|
import { NlpCloudExecutor } from "./nlpcloud.ts";
|
||||||
import { PetalsExecutor } from "./petals.ts";
|
import { PetalsExecutor } from "./petals.ts";
|
||||||
@@ -39,6 +40,8 @@ const executors = {
|
|||||||
glmt: new GlmExecutor("glmt"),
|
glmt: new GlmExecutor("glmt"),
|
||||||
cu: new CursorExecutor(), // Alias for cursor
|
cu: new CursorExecutor(), // Alias for cursor
|
||||||
"azure-openai": new AzureOpenAIExecutor(),
|
"azure-openai": new AzureOpenAIExecutor(),
|
||||||
|
"command-code": new CommandCodeExecutor(),
|
||||||
|
cmd: new CommandCodeExecutor(), // Alias
|
||||||
gitlab: new GitlabExecutor(),
|
gitlab: new GitlabExecutor(),
|
||||||
"gitlab-duo": new GitlabExecutor("gitlab-duo"),
|
"gitlab-duo": new GitlabExecutor("gitlab-duo"),
|
||||||
nlpcloud: new NlpCloudExecutor(),
|
nlpcloud: new NlpCloudExecutor(),
|
||||||
@@ -105,6 +108,7 @@ export { ChatGptWebExecutor } from "./chatgpt-web.ts";
|
|||||||
export { BlackboxWebExecutor } from "./blackbox-web.ts";
|
export { BlackboxWebExecutor } from "./blackbox-web.ts";
|
||||||
export { MuseSparkWebExecutor } from "./muse-spark-web.ts";
|
export { MuseSparkWebExecutor } from "./muse-spark-web.ts";
|
||||||
export { AzureOpenAIExecutor } from "./azure-openai.ts";
|
export { AzureOpenAIExecutor } from "./azure-openai.ts";
|
||||||
|
export { CommandCodeExecutor } from "./commandCode.ts";
|
||||||
export { GitlabExecutor } from "./gitlab.ts";
|
export { GitlabExecutor } from "./gitlab.ts";
|
||||||
export { NlpCloudExecutor } from "./nlpcloud.ts";
|
export { NlpCloudExecutor } from "./nlpcloud.ts";
|
||||||
export { PetalsExecutor } from "./petals.ts";
|
export { PetalsExecutor } from "./petals.ts";
|
||||||
|
|||||||
42
public/providers/command-code.svg
Normal file
42
public/providers/command-code.svg
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M2305 6994 c-371 -13 -682 -39 -893 -74 -598 -103 -963 -350 -1172
|
||||||
|
-795 -126 -267 -186 -576 -222 -1130 -19 -287 -18 -2669 0 -2950 53 -794 172
|
||||||
|
-1175 464 -1481 286 -298 672 -437 1367 -489 616 -47 2694 -46 3267 0 685 56
|
||||||
|
1056 186 1339 470 289 289 424 685 475 1395 22 310 32 1055 27 1915 -6 868
|
||||||
|
-13 1102 -42 1417 -55 589 -188 950 -448 1216 -305 311 -678 435 -1487 493
|
||||||
|
-141 10 -2428 21 -2675 13z m33 -1350 c322 -66 580 -324 646 -646 12 -57 16
|
||||||
|
-136 16 -303 l0 -225 474 0 474 0 5 243 c4 201 8 255 25 318 67 248 222 437
|
||||||
|
447 545 138 67 195 79 370 79 143 -1 154 -2 245 -33 279 -96 476 -307 552
|
||||||
|
-587 29 -111 29 -297 -1 -410 -69 -261 -263 -478 -511 -572 -113 -43 -194 -53
|
||||||
|
-431 -53 l-219 0 0 -474 0 -474 238 -5 c253 -5 310 -14 432 -64 243 -99 438
|
||||||
|
-327 495 -576 47 -209 17 -419 -88 -607 -57 -102 -205 -250 -307 -307 -433
|
||||||
|
-242 -960 -71 -1169 379 -63 134 -74 200 -79 466 l-4 232 -474 0 -474 0 0
|
||||||
|
-219 c0 -149 -5 -243 -14 -294 -60 -312 -299 -565 -611 -649 -112 -30 -298
|
||||||
|
-30 -410 0 -519 139 -776 708 -534 1185 106 210 301 365 538 429 63 17 117 21
|
||||||
|
319 25 l242 5 0 474 0 474 -225 0 c-252 0 -329 11 -452 62 -485 201 -664 796
|
||||||
|
-372 1232 116 174 310 306 514 349 93 20 248 21 343 1z"/>
|
||||||
|
<path d="M2080 5174 c-187 -50 -302 -241 -256 -425 31 -119 118 -216 231 -256
|
||||||
|
42 -15 84 -18 260 -18 l210 0 0 210 c0 176 -3 218 -18 260 -39 111 -136 200
|
||||||
|
-252 230 -72 18 -103 18 -175 -1z"/>
|
||||||
|
<path d="M4705 5176 c-75 -19 -125 -49 -178 -105 -86 -92 -91 -112 -95 -373
|
||||||
|
l-3 -228 185 0 c264 0 337 20 429 119 74 79 92 127 92 241 0 83 -3 102 -27
|
||||||
|
150 -74 150 -249 236 -403 196z"/>
|
||||||
|
<path d="M3000 3525 l0 -475 475 0 475 0 0 475 0 475 -475 0 -475 0 0 -475z"/>
|
||||||
|
<path d="M2051 2550 c-59 -22 -68 -27 -129 -84 -178 -167 -127 -463 98 -574
|
||||||
|
48 -24 67 -27 150 -27 114 0 162 18 241 92 99 92 119 165 119 428 l0 185 -212
|
||||||
|
0 c-184 -1 -220 -3 -267 -20z"/>
|
||||||
|
<path d="M4432 2348 l3 -224 33 -66 c38 -77 92 -130 171 -167 48 -22 70 -26
|
||||||
|
146 -26 82 0 97 3 157 33 77 38 130 92 167 171 22 47 26 70 26 146 0 76 -4 99
|
||||||
|
-26 146 -37 79 -90 133 -167 171 l-66 33 -224 3 -223 3 3 -223z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -556,6 +556,9 @@ interface AddApiKeyModalProps {
|
|||||||
isCompatible?: boolean;
|
isCompatible?: boolean;
|
||||||
isAnthropic?: boolean;
|
isAnthropic?: boolean;
|
||||||
isCcCompatible?: boolean;
|
isCcCompatible?: boolean;
|
||||||
|
isCommandCode?: boolean;
|
||||||
|
commandCodeAuthState?: CommandCodeAuthFlowState;
|
||||||
|
onStartCommandCodeAuth?: () => void;
|
||||||
onSave: (data: {
|
onSave: (data: {
|
||||||
name: string;
|
name: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -566,6 +569,23 @@ interface AddApiKeyModalProps {
|
|||||||
onClose: () => void;
|
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 {
|
interface EditConnectionModalConnection {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -981,6 +1001,14 @@ export default function ProviderDetailPage() {
|
|||||||
const [showOAuthModal, _setShowOAuthModal] = useState(false);
|
const [showOAuthModal, _setShowOAuthModal] = useState(false);
|
||||||
const [reauthConnection, setReauthConnection] = useState<ConnectionRowConnection | null>(null);
|
const [reauthConnection, setReauthConnection] = useState<ConnectionRowConnection | null>(null);
|
||||||
const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
|
const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
|
||||||
|
const [commandCodeAuthState, setCommandCodeAuthState] = useState<CommandCodeAuthFlowState>({
|
||||||
|
phase: "idle",
|
||||||
|
state: "",
|
||||||
|
authUrl: "",
|
||||||
|
callbackUrl: "",
|
||||||
|
expiresAt: null,
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showEditNodeModal, setShowEditNodeModal] = useState(false);
|
const [showEditNodeModal, setShowEditNodeModal] = useState(false);
|
||||||
const [selectedConnection, setSelectedConnection] = useState(null);
|
const [selectedConnection, setSelectedConnection] = useState(null);
|
||||||
@@ -1027,8 +1055,11 @@ export default function ProviderDetailPage() {
|
|||||||
const [savingCodexGlobalFastServiceTier, setSavingCodexGlobalFastServiceTier] = useState(false);
|
const [savingCodexGlobalFastServiceTier, setSavingCodexGlobalFastServiceTier] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||||
|
const commandCodeAuthWindowRef = useRef<Window | null>(null);
|
||||||
|
const commandCodeAuthTimerRef = useRef<number | null>(null);
|
||||||
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
|
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
|
||||||
const isCcCompatible = isClaudeCodeCompatibleProvider(providerId);
|
const isCcCompatible = isClaudeCodeCompatibleProvider(providerId);
|
||||||
|
const isCommandCode = providerId === "command-code";
|
||||||
const isAnthropicCompatible =
|
const isAnthropicCompatible =
|
||||||
isAnthropicCompatibleProvider(providerId) && !isClaudeCodeCompatibleProvider(providerId);
|
isAnthropicCompatibleProvider(providerId) && !isClaudeCodeCompatibleProvider(providerId);
|
||||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible;
|
const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible;
|
||||||
@@ -1436,6 +1467,257 @@ export default function ProviderDetailPage() {
|
|||||||
setShowAddApiKeyModal(true);
|
setShowAddApiKeyModal(true);
|
||||||
}, [isOAuth]);
|
}, [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) => {
|
const handleSaveApiKey = async (formData) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/providers", {
|
const res = await fetch("/api/providers", {
|
||||||
@@ -2950,13 +3232,44 @@ export default function ProviderDetailPage() {
|
|||||||
)}
|
)}
|
||||||
{!isCompatible ? (
|
{!isCompatible ? (
|
||||||
<>
|
<>
|
||||||
<Button size="sm" icon="add" onClick={openPrimaryAddFlow}>
|
{isCommandCode ? (
|
||||||
{providerSupportsPat ? "Add PAT" : t("add")}
|
<>
|
||||||
</Button>
|
<Button
|
||||||
{providerId === "qoder" && (
|
size="sm"
|
||||||
<Button size="sm" variant="secondary" onClick={() => setShowOAuthModal(true)}>
|
icon="open_in_new"
|
||||||
Experimental OAuth
|
loading={
|
||||||
</Button>
|
commandCodeAuthState.phase === "starting" ||
|
||||||
|
commandCodeAuthState.phase === "polling" ||
|
||||||
|
commandCodeAuthState.phase === "applying"
|
||||||
|
}
|
||||||
|
onClick={handleOpenCommandCodeConnect}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
icon="add"
|
||||||
|
onClick={() => setShowAddApiKeyModal(true)}
|
||||||
|
>
|
||||||
|
Manual API key
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button size="sm" icon="add" onClick={openPrimaryAddFlow}>
|
||||||
|
{providerSupportsPat ? "Add PAT" : t("add")}
|
||||||
|
</Button>
|
||||||
|
{providerId === "qoder" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setShowOAuthModal(true)}
|
||||||
|
>
|
||||||
|
Experimental OAuth
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -2980,13 +3293,38 @@ export default function ProviderDetailPage() {
|
|||||||
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
|
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
|
||||||
{!isCompatible && (
|
{!isCompatible && (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Button icon="add" onClick={openPrimaryAddFlow}>
|
{isCommandCode ? (
|
||||||
{providerSupportsPat ? "Add PAT" : t("addConnection")}
|
<>
|
||||||
</Button>
|
<Button
|
||||||
{providerId === "qoder" && (
|
icon="open_in_new"
|
||||||
<Button variant="secondary" onClick={() => setShowOAuthModal(true)}>
|
loading={
|
||||||
Experimental OAuth
|
commandCodeAuthState.phase === "starting" ||
|
||||||
</Button>
|
commandCodeAuthState.phase === "polling" ||
|
||||||
|
commandCodeAuthState.phase === "applying"
|
||||||
|
}
|
||||||
|
onClick={handleOpenCommandCodeConnect}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon="add"
|
||||||
|
onClick={() => setShowAddApiKeyModal(true)}
|
||||||
|
>
|
||||||
|
Manual API key
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button icon="add" onClick={openPrimaryAddFlow}>
|
||||||
|
{providerSupportsPat ? "Add PAT" : t("addConnection")}
|
||||||
|
</Button>
|
||||||
|
{providerId === "qoder" && (
|
||||||
|
<Button variant="secondary" onClick={() => setShowOAuthModal(true)}>
|
||||||
|
Experimental OAuth
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -3409,8 +3747,11 @@ export default function ProviderDetailPage() {
|
|||||||
isCompatible={isCompatible}
|
isCompatible={isCompatible}
|
||||||
isAnthropic={isAnthropicProtocolCompatible}
|
isAnthropic={isAnthropicProtocolCompatible}
|
||||||
isCcCompatible={isCcCompatible}
|
isCcCompatible={isCcCompatible}
|
||||||
|
isCommandCode={isCommandCode}
|
||||||
|
commandCodeAuthState={commandCodeAuthState}
|
||||||
|
onStartCommandCodeAuth={handleStartCommandCodeAuth}
|
||||||
onSave={handleSaveApiKey}
|
onSave={handleSaveApiKey}
|
||||||
onClose={() => setShowAddApiKeyModal(false)}
|
onClose={handleCloseAddApiKeyModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isUpstreamProxyProvider && (
|
{!isUpstreamProxyProvider && (
|
||||||
@@ -5715,6 +6056,52 @@ function formatExcludedModelsInput(value: unknown): string {
|
|||||||
.join(", ");
|
.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<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
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({
|
function AddApiKeyModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
provider,
|
provider,
|
||||||
@@ -5722,6 +6109,9 @@ function AddApiKeyModal({
|
|||||||
isCompatible,
|
isCompatible,
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
isCcCompatible,
|
isCcCompatible,
|
||||||
|
isCommandCode,
|
||||||
|
commandCodeAuthState,
|
||||||
|
onStartCommandCodeAuth,
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
}: AddApiKeyModalProps) {
|
}: AddApiKeyModalProps) {
|
||||||
@@ -5744,6 +6134,18 @@ function AddApiKeyModal({
|
|||||||
const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb;
|
const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb;
|
||||||
const isPetals = provider === "petals";
|
const isPetals = provider === "petals";
|
||||||
const apiKeyOptional = isSearxng || isPetals || isLocalSelfHostedProvider;
|
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({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -5767,6 +6169,7 @@ function AddApiKeyModal({
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
const [copiedCommandCodeField, setCopiedCommandCodeField] = useState<string | null>(null);
|
||||||
const apiCredentialLabel = isQoder
|
const apiCredentialLabel = isQoder
|
||||||
? t("personalAccessTokenLabel")
|
? t("personalAccessTokenLabel")
|
||||||
: isWebSessionProvider
|
: isWebSessionProvider
|
||||||
@@ -5811,12 +6214,15 @@ function AddApiKeyModal({
|
|||||||
setValidating(true);
|
setValidating(true);
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
try {
|
try {
|
||||||
|
const credentialInput = isCommandCode
|
||||||
|
? extractCommandCodeCredentialInput(formData.apiKey)
|
||||||
|
: formData.apiKey;
|
||||||
const res = await fetch("/api/providers/validate", {
|
const res = await fetch("/api/providers/validate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
provider,
|
provider,
|
||||||
apiKey: formData.apiKey,
|
apiKey: credentialInput,
|
||||||
validationModelId: formData.validationModelId || undefined,
|
validationModelId: formData.validationModelId || undefined,
|
||||||
customUserAgent: formData.customUserAgent.trim() || undefined,
|
customUserAgent: formData.customUserAgent.trim() || undefined,
|
||||||
baseUrl: formData.baseUrl.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 () => {
|
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);
|
setSaving(true);
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
@@ -5863,7 +6283,7 @@ function AddApiKeyModal({
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
provider,
|
provider,
|
||||||
apiKey: formData.apiKey,
|
apiKey: credentialInput,
|
||||||
validationModelId: formData.validationModelId || undefined,
|
validationModelId: formData.validationModelId || undefined,
|
||||||
customUserAgent: formData.customUserAgent.trim() || undefined,
|
customUserAgent: formData.customUserAgent.trim() || undefined,
|
||||||
baseUrl: formData.baseUrl.trim() || undefined,
|
baseUrl: formData.baseUrl.trim() || undefined,
|
||||||
@@ -5883,7 +6303,7 @@ function AddApiKeyModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
if (apiKeyOptional && !formData.apiKey) {
|
if (apiKeyOptional && !credentialInput) {
|
||||||
// Bypass validation block for local/optional providers when no key is provided
|
// Bypass validation block for local/optional providers when no key is provided
|
||||||
console.debug("Validation failed but apiKey is optional; proceeding to save.");
|
console.debug("Validation failed but apiKey is optional; proceeding to save.");
|
||||||
} else {
|
} else {
|
||||||
@@ -5926,7 +6346,7 @@ function AddApiKeyModal({
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
apiKey: formData.apiKey.trim() || undefined,
|
apiKey: credentialInput.trim() || undefined,
|
||||||
priority: formData.priority,
|
priority: formData.priority,
|
||||||
testStatus: "active",
|
testStatus: "active",
|
||||||
providerSpecificData:
|
providerSpecificData:
|
||||||
@@ -5961,6 +6381,84 @@ function AddApiKeyModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isCommandCode && onStartCommandCodeAuth && (
|
||||||
|
<div className="rounded-lg border border-sky-500/20 bg-sky-500/10 px-3 py-3 text-sm">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="material-symbols-outlined mt-0.5 text-[18px] text-sky-500">
|
||||||
|
open_in_new
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-text-main">Browser/manual connect</p>
|
||||||
|
<p className="mt-1 text-xs text-text-muted">
|
||||||
|
Open Command Code Studio, then paste the returned key/JSON/URL into the API key
|
||||||
|
field below.
|
||||||
|
</p>
|
||||||
|
{commandCodeAuthState?.message && (
|
||||||
|
<p className="mt-2 text-xs text-text-muted">
|
||||||
|
{commandCodeAuthPhaseLabel}: {commandCodeAuthState.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{commandCodeAuthState?.authUrl && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div>
|
||||||
|
<p className="mb-1 text-xs font-medium text-text-main">Auth URL</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={commandCodeAuthState.authUrl}
|
||||||
|
readOnly
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon={copiedCommandCodeField === "authUrl" ? "check" : "content_copy"}
|
||||||
|
onClick={() =>
|
||||||
|
copyCommandCodeValue(commandCodeAuthState.authUrl, "authUrl")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{commandCodeAuthState.callbackUrl && (
|
||||||
|
<div>
|
||||||
|
<p className="mb-1 text-xs font-medium text-text-main">Callback URL</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={commandCodeAuthState.callbackUrl}
|
||||||
|
readOnly
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon={
|
||||||
|
copiedCommandCodeField === "callbackUrl" ? "check" : "content_copy"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
copyCommandCodeValue(commandCodeAuthState.callbackUrl, "callbackUrl")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
icon="open_in_new"
|
||||||
|
loading={
|
||||||
|
commandCodeAuthState?.phase === "starting" ||
|
||||||
|
commandCodeAuthState?.phase === "polling" ||
|
||||||
|
commandCodeAuthState?.phase === "applying"
|
||||||
|
}
|
||||||
|
onClick={onStartCommandCodeAuth}
|
||||||
|
>
|
||||||
|
Connect in browser
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
label={t("nameLabel")}
|
label={t("nameLabel")}
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
|
|||||||
101
src/app/api/providers/command-code/auth/apply/route.ts
Normal file
101
src/app/api/providers/command-code/auth/apply/route.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||||
|
import { consumeCommandCodeAuthSecret } from "@/lib/db/commandCodeAuth";
|
||||||
|
import {
|
||||||
|
createProviderConnection,
|
||||||
|
getProviderConnectionById,
|
||||||
|
updateProviderConnection,
|
||||||
|
} from "@/lib/db/providers";
|
||||||
|
import { sanitizeProviderSpecificDataForResponse } from "@/lib/providers/requestDefaults";
|
||||||
|
|
||||||
|
import { commandCodeApplySchema, noStoreJson, stateHashFromState } from "../shared";
|
||||||
|
|
||||||
|
function safeConnection(
|
||||||
|
connection: Record<string, unknown> | null
|
||||||
|
): Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | null) || {}),
|
||||||
|
authAssist: {
|
||||||
|
userId: consumed.metadata?.userId,
|
||||||
|
userName: consumed.metadata?.userName,
|
||||||
|
keyName: consumed.metadata?.keyName,
|
||||||
|
appliedAt: consumed.appliedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(parsed.data.setDefault ? { priority: 1 } : {}),
|
||||||
|
})) as Record<string, unknown> | 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<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return noStoreJson({ connection: safeConnection(connection), status: "applied" });
|
||||||
|
}
|
||||||
67
src/app/api/providers/command-code/auth/callback/route.ts
Normal file
67
src/app/api/providers/command-code/auth/callback/route.ts
Normal file
@@ -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) }
|
||||||
|
);
|
||||||
|
}
|
||||||
120
src/app/api/providers/command-code/auth/shared.ts
Normal file
120
src/app/api/providers/command-code/auth/shared.ts
Normal file
@@ -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<string, string> = {
|
||||||
|
"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<unknown> {
|
||||||
|
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`;
|
||||||
|
}
|
||||||
28
src/app/api/providers/command-code/auth/start/route.ts
Normal file
28
src/app/api/providers/command-code/auth/start/route.ts
Normal file
@@ -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" });
|
||||||
|
}
|
||||||
38
src/app/api/providers/command-code/auth/status/route.ts
Normal file
38
src/app/api/providers/command-code/auth/status/route.ts
Normal file
@@ -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<string | null> {
|
||||||
|
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;
|
||||||
213
src/lib/db/commandCodeAuth.ts
Normal file
213
src/lib/db/commandCodeAuth.ts
Normal file
@@ -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<TRow = unknown> = {
|
||||||
|
get: (...params: unknown[]) => TRow | undefined;
|
||||||
|
all: (...params: unknown[]) => TRow[];
|
||||||
|
run: (...params: unknown[]) => DbRunResult;
|
||||||
|
};
|
||||||
|
type DbLike = {
|
||||||
|
prepare: <TRow = unknown>(sql: string) => DbStatement<TRow>;
|
||||||
|
transaction: <T extends (...args: unknown[]) => 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<string, unknown>;
|
||||||
|
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<AuthSessionRow>("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<AuthSessionRow>("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<AuthSessionRow>(
|
||||||
|
`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;
|
||||||
|
}
|
||||||
19
src/lib/db/migrations/055_command_code_auth_sessions.sql
Normal file
19
src/lib/db/migrations/055_command_code_auth_sessions.sql
Normal file
@@ -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);
|
||||||
@@ -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 { getEmbeddingProvider } from "@omniroute/open-sse/config/embeddingRegistry.ts";
|
||||||
import { getRerankProvider } from "@omniroute/open-sse/config/rerankRegistry.ts";
|
import { getRerankProvider } from "@omniroute/open-sse/config/rerankRegistry.ts";
|
||||||
|
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||||
import {
|
import {
|
||||||
buildClaudeCodeCompatibleHeaders,
|
buildClaudeCodeCompatibleHeaders,
|
||||||
buildClaudeCodeCompatibleValidationPayload,
|
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) {
|
async function validateClarifaiProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.clarifai.com/v2/ext/openai/v1";
|
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 = {
|
const SPECIALTY_VALIDATORS = {
|
||||||
qoder: ({ apiKey, providerSpecificData }: any) =>
|
qoder: ({ apiKey, providerSpecificData }: any) =>
|
||||||
validateQoderCliPat({ apiKey, providerSpecificData }),
|
validateQoderCliPat({ apiKey, providerSpecificData }),
|
||||||
|
"command-code": validateCommandCodeProvider,
|
||||||
deepgram: validateDeepgramProvider,
|
deepgram: validateDeepgramProvider,
|
||||||
assemblyai: validateAssemblyAIProvider,
|
assemblyai: validateAssemblyAIProvider,
|
||||||
nanobanana: validateNanoBananaProvider,
|
nanobanana: validateNanoBananaProvider,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const KNOWN_SVGS = new Set([
|
|||||||
"brave-search",
|
"brave-search",
|
||||||
"cartesia",
|
"cartesia",
|
||||||
"clarifai",
|
"clarifai",
|
||||||
|
"command-code",
|
||||||
"docker-model-runner",
|
"docker-model-runner",
|
||||||
"droid",
|
"droid",
|
||||||
"gemini-cli",
|
"gemini-cli",
|
||||||
|
|||||||
@@ -184,6 +184,18 @@ export const APIKEY_PROVIDERS = {
|
|||||||
freeNote: "$200 free credits on signup - multi-model routing gateway",
|
freeNote: "$200 free credits on signup - multi-model routing gateway",
|
||||||
apiHint: "Get $200 free credits at https://agentrouter.org/register — no credit card required.",
|
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: {
|
openrouter: {
|
||||||
id: "openrouter",
|
id: "openrouter",
|
||||||
alias: "openrouter",
|
alias: "openrouter",
|
||||||
|
|||||||
231
tests/unit/command-code-auth-assist.test.ts
Normal file
231
tests/unit/command-code-auth-assist.test.ts
Normal file
@@ -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");
|
||||||
|
});
|
||||||
249
tests/unit/command-code-executor.test.ts
Normal file
249
tests/unit/command-code-executor.test.ts
Normal file
@@ -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/);
|
||||||
|
});
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
const { validateProviderApiKey, validateClaudeCodeCompatibleProvider } =
|
const {
|
||||||
await import("../../src/lib/providers/validation.ts");
|
validateProviderApiKey,
|
||||||
|
validateClaudeCodeCompatibleProvider,
|
||||||
|
validateCommandCodeProvider,
|
||||||
|
} = await import("../../src/lib/providers/validation.ts");
|
||||||
|
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
@@ -10,6 +13,13 @@ test.afterEach(() => {
|
|||||||
globalThis.fetch = originalFetch;
|
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") {
|
function metaAiSseText(content: string, streamingState = "DONE") {
|
||||||
return `event: next
|
return `event: next
|
||||||
data: ${JSON.stringify({
|
data: ${JSON.stringify({
|
||||||
@@ -73,6 +83,28 @@ test("specialty provider validators cover Deepgram, AssemblyAI, NanoBanana, Elev
|
|||||||
assert.equal(inworld.valid, true);
|
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<string, string>;
|
||||||
|
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 () => {
|
test("specialty providers surface network failures and non-auth upstream failures", async () => {
|
||||||
globalThis.fetch = async (url) => {
|
globalThis.fetch = async (url) => {
|
||||||
const target = String(url);
|
const target = String(url);
|
||||||
@@ -1876,3 +1908,63 @@ test("specialty validator rejects invalid Runway credentials", async () => {
|
|||||||
|
|
||||||
assert.equal(runway.error, "Invalid API key");
|
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)",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -224,6 +224,45 @@ test("handleResponsesCore transforms upstream OpenAI SSE into Responses API SSE"
|
|||||||
assert.match(sse, /data: \[DONE\]/);
|
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 () => {
|
test("handleResponsesCore propagates upstream failures from chatCore unchanged", async () => {
|
||||||
const { result } = await invokeResponsesCore({
|
const { result } = await invokeResponsesCore({
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
Reference in New Issue
Block a user