Skip to content

Commit 4b95fdb

Browse files
committed
fix: harden Command Code capabilities
1 parent 4505210 commit 4b95fdb

12 files changed

Lines changed: 328 additions & 48 deletions

File tree

gui/src/i18n/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const de: Record<TKey, string> = {
3737
"theme.dark": "Dunkel",
3838
"theme.system": "System",
3939
"lang.label": "Sprache",
40+
"provider.name.commandCode": "Command Code",
4041
"provider.name.volcengine": "Volcengine Ark",
4142
"provider.name.volcengineCodingPlan": "Volcengine Ark Coding-Tarif",
4243
"provider.name.volcengineAgentPlan": "Volcengine Ark Agent-Tarif",

gui/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const en = {
4444
"theme.dark": "Dark",
4545
"theme.system": "System",
4646
"lang.label": "Language",
47+
"provider.name.commandCode": "Command Code",
4748
"provider.name.volcengine": "Volcengine Ark",
4849
"provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan",
4950
"provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan",

gui/src/i18n/ja.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const ja: Record<TKey, string> = {
4242
"theme.dark": "ダーク",
4343
"theme.system": "システム",
4444
"lang.label": "言語",
45+
"provider.name.commandCode": "Command Code",
4546
"provider.name.volcengine": "Volcengine Ark",
4647
"provider.name.volcengineCodingPlan": "Volcengine Ark コーディングプラン",
4748
"provider.name.volcengineAgentPlan": "Volcengine Ark エージェントプラン",

gui/src/i18n/ko.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const ko: Record<TKey, string> = {
3737
"theme.dark": "다크",
3838
"theme.system": "시스템",
3939
"lang.label": "언어",
40+
"provider.name.commandCode": "Command Code",
4041
"provider.name.volcengine": "Volcengine Ark",
4142
"provider.name.volcengineCodingPlan": "Volcengine Ark 코딩 플랜",
4243
"provider.name.volcengineAgentPlan": "Volcengine Ark 에이전트 플랜",

gui/src/i18n/ru.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const ru: Record<TKey, string> = {
4242
"theme.dark": "Тёмная",
4343
"theme.system": "Системная",
4444
"lang.label": "Язык",
45+
"provider.name.commandCode": "Command Code",
4546
"provider.name.volcengine": "Volcengine Ark",
4647
"provider.name.volcengineCodingPlan": "Volcengine Ark — тариф Coding",
4748
"provider.name.volcengineAgentPlan": "Volcengine Ark — тариф Agent",

gui/src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const zh: Record<TKey, string> = {
3737
"theme.dark": "深色",
3838
"theme.system": "跟随系统",
3939
"lang.label": "语言",
40+
"provider.name.commandCode": "Command Code",
4041
"provider.name.volcengine": "火山方舟",
4142
"provider.name.volcengineCodingPlan": "火山方舟编程套餐",
4243
"provider.name.volcengineAgentPlan": "火山方舟智能体套餐",

gui/src/provider-icons.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
6363
"cloudflare-workers-ai": "Cloudflare Workers AI",
6464
cline: "Cline",
6565
"cline-pass": "ClinePass",
66-
"command-code": "Command Code",
67-
commandcode: "Command Code",
6866
nvidia: "NVIDIA NIM",
6967
ollama: "Ollama",
7068
"ollama-cloud": "Ollama Cloud",
@@ -101,6 +99,8 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
10199
};
102100

103101
const PROVIDER_DISPLAY_NAME_KEYS: Record<string, TKey> = {
102+
"command-code": "provider.name.commandCode",
103+
commandcode: "provider.name.commandCode",
104104
volcengine: "provider.name.volcengine",
105105
"volcengine-coding-plan": "provider.name.volcengineCodingPlan",
106106
"volcengine-agent-plan": "provider.name.volcengineAgentPlan",

src/adapters/command-code.ts

Lines changed: 179 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { randomUUID } from "node:crypto";
22
import { readdirSync } from "node:fs";
33
import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
4-
import { namespacedToolName } from "../types";
5-
import type { AdapterRequest, ProviderAdapter } from "./base";
4+
import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
5+
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
66
import type { TranslatorBudget } from "../lib/translator-budget";
7+
import { readBoundedResponseBody } from "../lib/bounded-body";
8+
import { configuredReasoningEfforts } from "../reasoning-effort";
9+
import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
10+
import { identifyRoutedModel } from "./identity";
11+
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
712

813
// Retain the short ids emitted by the first local integration. New requests use the live catalog's
914
// provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
@@ -50,19 +55,50 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
5055
return out;
5156
}
5257

53-
function wireTools(tools: OcxTool[] | undefined): Array<Record<string, unknown>> {
54-
return (tools ?? []).map(tool => ({
58+
function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
59+
const choice = parsed.options.toolChoice;
60+
if (choice === "none") return [];
61+
const tools = parsed.context.tools ?? [];
62+
if (isAllowedToolChoice(choice)) {
63+
const allowed = new Set(choice.allowedTools);
64+
return tools.filter(tool => toolAllowedByChoice(tool, allowed));
65+
}
66+
if (choice && typeof choice !== "string") {
67+
return tools.filter(tool => tool.name === choice.name || namespacedToolName(tool.namespace, tool.name) === choice.name);
68+
}
69+
return tools;
70+
}
71+
72+
function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined {
73+
const choice = parsed.options.toolChoice;
74+
if (choice === "required" || (isAllowedToolChoice(choice) && choice.mode === "required")) {
75+
return "Tool choice is required for this turn. Make at least one call from the advertised tool catalog before answering.";
76+
}
77+
if (choice && typeof choice !== "string" && !isAllowedToolChoice(choice)) {
78+
return `Tool choice is required for this turn. Call the advertised tool named ${namespacedToolName(undefined, choice.name)} before answering.`;
79+
}
80+
return undefined;
81+
}
82+
83+
function wireTools(tools: OcxTool[]): Array<Record<string, unknown>> {
84+
return tools.map(tool => ({
5585
name: namespacedToolName(tool.namespace, tool.name),
5686
description: tool.description,
5787
input_schema: tool.parameters,
5888
}));
5989
}
6090

61-
function commandCodeConfig(): Record<string, unknown> {
91+
function currentWorkingDirectory(): string | undefined {
92+
try { return process.cwd(); } catch { return undefined; }
93+
}
94+
95+
function commandCodeConfig(cwd: string | undefined): Record<string, unknown> {
6296
let structure: string[] = [];
63-
try { structure = readdirSync(process.cwd()).filter(name => !name.startsWith(".")); } catch { /* cwd may disappear */ }
97+
if (cwd) {
98+
try { structure = readdirSync(cwd).filter(name => !name.startsWith(".")); } catch { /* workspace metadata is optional */ }
99+
}
64100
return {
65-
workingDir: process.cwd(),
101+
...(cwd ? { workingDir: cwd } : {}),
66102
date: new Date().toISOString().slice(0, 10),
67103
environment: process.platform,
68104
structure,
@@ -99,74 +135,179 @@ function eventError(value: unknown): string {
99135
return "Command Code stream error";
100136
}
101137

102-
async function*ndjson(response: Response): AsyncGenerator<Record<string, unknown>> {
138+
async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenerator<Record<string, unknown>> {
103139
if (!response.body) throw new Error("Command Code response body missing");
104140
const reader = response.body.getReader();
105141
const decoder = new TextDecoder();
142+
const encoder = new TextEncoder();
106143
let buffer = "";
107-
for (;;) {
108-
const { value, done } = await reader.read();
109-
buffer += decoder.decode(value, { stream: !done });
110-
let newline = buffer.indexOf("\n");
111-
while (newline >= 0) {
112-
const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
113-
if (line) { try { yield JSON.parse(line) as Record<string, unknown>; } catch { /* ignore non-events */ } }
114-
newline = buffer.indexOf("\n");
144+
let bufferBytes = 0;
145+
try {
146+
for (;;) {
147+
const { value, done } = await reader.read();
148+
const next = buffer + decoder.decode(value, { stream: !done });
149+
const nextBytes = encoder.encode(next).byteLength;
150+
const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" });
151+
buffer = next;
152+
reservation.commitRetained();
153+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
154+
bufferBytes = nextBytes;
155+
let newline = buffer.indexOf("\n");
156+
while (newline >= 0) {
157+
const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
158+
if (line) { try { yield JSON.parse(line) as Record<string, unknown>; } catch { /* ignore non-events */ } }
159+
newline = buffer.indexOf("\n");
160+
}
161+
const residualBytes = encoder.encode(buffer).byteLength;
162+
const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" });
163+
residualReservation.commitRetained();
164+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
165+
bufferBytes = residualBytes;
166+
if (done) break;
115167
}
116-
if (done) break;
168+
const final = buffer.trim();
169+
if (final) { try { yield JSON.parse(final) as Record<string, unknown>; } catch { /* ignore */ } }
170+
} finally {
171+
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
172+
reader.releaseLock();
117173
}
118-
const final = buffer.trim();
119-
if (final) { try { yield JSON.parse(final) as Record<string, unknown>; } catch { /* ignore */ } }
174+
}
175+
176+
function isReasoningEffortRejection(status: number, payload: string): boolean {
177+
return (status === 400 || status === 422) && /reasoning[_ -]?effort|unsupported effort|invalid effort/i.test(payload);
178+
}
179+
180+
function requestWithoutReasoningEffort(request: AdapterRequest): AdapterRequest | undefined {
181+
try {
182+
const body = JSON.parse(request.body) as { params?: Record<string, unknown> };
183+
if (!body.params?.reasoning_effort) return undefined;
184+
delete body.params.reasoning_effort;
185+
return { ...request, body: JSON.stringify(body), reasoningLog: undefined };
186+
} catch {
187+
return undefined;
188+
}
189+
}
190+
191+
async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContext | undefined, executor: typeof globalThis.fetch): Promise<Response> {
192+
const timeout = new AbortController();
193+
const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000);
194+
const callerSignal = ctx?.abortSignal ?? new AbortController().signal;
195+
try {
196+
return await executor(request.url, {
197+
method: request.method,
198+
headers: request.headers,
199+
body: request.body,
200+
redirect: "manual",
201+
signal: AbortSignal.any([callerSignal, timeout.signal]),
202+
});
203+
} finally {
204+
clearTimeout(timer);
205+
}
206+
}
207+
208+
function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
209+
if (!requested || requested === "none") return undefined;
210+
const supported = commandCodeReasoningEfforts(modelId) ?? configuredReasoningEfforts(provider, modelId);
211+
if (!supported) return undefined;
212+
// Command Code's official profiles describe xhigh as the CLI label that maps to
213+
// the wire value `max`; preserve that mapping without advertising a synthetic tier.
214+
const wire = requested === "xhigh" && supported.includes("max") ? "max" : requested;
215+
return supported.includes(wire) ? wire : undefined;
120216
}
121217

122218
export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter {
219+
const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
123220
return {
124221
name: "command-code",
125222
buildRequest(parsed: OcxParsedRequest): AdapterRequest {
126223
if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
127-
const system = parsed.context.systemPrompt?.join("\n\n") ?? "";
224+
const cwd = currentWorkingDirectory();
225+
const tools = visibleTools(parsed);
226+
const toolNudge = buildNonOpenAIToolCatalogNudgeForTools(tools, parsed.options.toolChoice);
227+
const choiceInstruction = toolChoiceInstruction(parsed);
228+
const system = identifyRoutedModel([
229+
...(parsed.context.systemPrompt ?? []),
230+
...(toolNudge ? [toolNudge] : []),
231+
...(choiceInstruction ? [choiceInstruction] : []),
232+
].join("\n\n"), parsed.modelId);
233+
const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
128234
const body = {
129-
config: commandCodeConfig(), memory: null, taste: null, skills: null,
235+
config: commandCodeConfig(cwd), memory: null, taste: null, skills: null,
130236
permissionMode: "standard", mode: "agent",
131237
params: {
132238
model: COMMAND_CODE_MODEL_ALIASES[parsed.modelId] ?? parsed.modelId,
133239
messages: wireMessages(parsed.context.messages),
134-
tools: wireTools(parsed.context.tools),
240+
tools: wireTools(tools),
135241
system,
136242
max_tokens: parsed.options.maxOutputTokens ?? provider.defaultMaxOutputTokens ?? 64_000,
137243
stream: true,
138244
...(parsed.options.temperature !== undefined ? { temperature: parsed.options.temperature } : {}),
139-
...(parsed.options.reasoning && parsed.options.reasoning !== "none" ? { reasoning_effort: parsed.options.reasoning } : {}),
245+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
140246
},
141247
};
248+
const headers: Record<string, string> = {
249+
Authorization: `Bearer ${provider.apiKey}`,
250+
"Content-Type": "application/json",
251+
"User-Agent": "cli",
252+
"x-command-code-version": "1.12.0",
253+
"x-cli-environment": "production",
254+
"x-taste-learning": "false",
255+
"x-co-flag": "false",
256+
"x-session-id": randomUUID(),
257+
};
258+
if (cwd) headers["x-project-slug"] = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
142259
return {
143260
url: `${provider.baseUrl.replace(/\/$/, "")}/alpha/generate`, method: "POST",
144-
headers: {
145-
Authorization: `Bearer ${provider.apiKey}`,
146-
"Content-Type": "application/json",
147-
"User-Agent": "cli",
148-
"x-command-code-version": "1.12.0",
149-
"x-cli-environment": "production",
150-
"x-project-slug": process.cwd().replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase(),
151-
"x-taste-learning": "false",
152-
"x-co-flag": "false",
153-
"x-session-id": randomUUID(),
154-
},
261+
headers,
155262
body: JSON.stringify(body),
263+
...(reasoningEffort ? { reasoningLog: { effectiveEffort: reasoningEffort, wireField: "reasoning_effort" as const, wireValue: reasoningEffort } } : {}),
156264
};
157265
},
158-
async *parseStream(response: Response, _budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
159-
for await (const event of ndjson(response)) {
266+
async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
267+
const response = await fetchCommandCode(request, ctx, executor);
268+
if (response.ok) return response;
269+
const currentEffort = (() => {
270+
try { return (JSON.parse(request.body) as { params?: { reasoning_effort?: unknown } }).params?.reasoning_effort; } catch { return undefined; }
271+
})();
272+
if (typeof currentEffort !== "string") return response;
273+
let body = "";
274+
try {
275+
const observed = await readBoundedResponseBody(response.clone(), { signal: ctx?.abortSignal, maxBytes: 8 * 1024 });
276+
if (!observed.displaySafe) return response;
277+
body = observed.text;
278+
} catch { return response; }
279+
if (!isReasoningEffortRejection(response.status, body)) return response;
280+
const modelId = (() => {
281+
try { return (JSON.parse(request.body) as { params?: { model?: unknown } }).params?.model; } catch { return undefined; }
282+
})();
283+
if (typeof modelId !== "string") return response;
284+
const refreshed = await refreshCommandCodeReasoningEfforts(modelId, executor);
285+
if (!refreshed || refreshed.includes(currentEffort)) return response;
286+
const retry = requestWithoutReasoningEffort(request);
287+
if (!retry) return response;
288+
try { void response.body?.cancel(); } catch { /* already closed */ }
289+
return fetchCommandCode(retry, ctx, executor);
290+
},
291+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
292+
for await (const event of ndjson(response, budget)) {
160293
switch (event.type) {
161294
case "text-delta": if (typeof event.text === "string") yield { type: "text_delta", text: event.text }; break;
162295
case "reasoning-delta": if (typeof event.text === "string") yield { type: "thinking_delta", thinking: event.text }; break;
163296
case "tool-call": {
164297
const id = typeof event.toolCallId === "string" ? event.toolCallId : randomUUID();
165298
const name = typeof event.toolName === "string" ? event.toolName : "tool";
166299
const input = event.input ?? event.args ?? {};
300+
const argumentsText = typeof input === "string" ? input : JSON.stringify(input);
167301
yield { type: "tool_call_start", id, name };
168-
yield { type: "tool_call_delta", arguments: typeof input === "string" ? input : JSON.stringify(input) };
169-
yield { type: "tool_call_end" };
302+
budget.openCall(id);
303+
try {
304+
const reservation = budget.reserveTransient(new TextEncoder().encode(argumentsText).byteLength, { kind: "tool_args", callId: id });
305+
reservation.commitRetained();
306+
yield { type: "tool_call_delta", arguments: argumentsText };
307+
yield { type: "tool_call_end" };
308+
} finally {
309+
budget.closeCall(id);
310+
}
170311
break;
171312
}
172313
case "finish": yield { type: "done", usage: usage(event.totalUsage), stopReason: typeof event.rawFinishReason === "string" ? event.rawFinishReason : undefined }; break;

src/oauth/command-code.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ function createCallbackServer(state: string): {
110110
}
111111

112112
export async function loginCommandCode(ctrl: OAuthController, options: CommandCodeLoginOptions = {}): Promise<OAuthCredentials> {
113+
if (ctrl.signal?.aborted) {
114+
throw ctrl.signal.reason ?? new DOMException("Command Code login aborted", "AbortError");
115+
}
113116
if (shouldImportLocalCommandCodeAuth(options)) {
114117
const local = await importLocalCommandCodeAuth();
115118
if (local) {
@@ -119,7 +122,7 @@ export async function loginCommandCode(ctrl: OAuthController, options: CommandCo
119122
}
120123
const state = randomState();
121124
const { server, callback } = createCallbackServer(state);
122-
const callbackUrl = `http://localhost:${server.port}/callback`;
125+
const callbackUrl = `http://127.0.0.1:${server.port}/callback`;
123126
const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
124127
ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." });
125128
ctrl.onProgress?.("Waiting for Command Code authentication...");

0 commit comments

Comments
 (0)