fix(commandcode): preserve images and reasoning_effort on /alpha/generate

Command Code dropped vision and ignored client effort through the router:
image blocks became "[image omitted]", HTTP image URLs were never inlined,
and reasoning_effort landed on the envelope wrapper instead of params (so the
DeepSeek family mapping remapped low -> high). The catalog also treated
deepseek/deepseek-v4.1-flash as text-only, so the vision adapter stole those
requests to another provider.

- Map OpenAI image_url / Claude image blocks (base64 or data-URI) to the
  native {type:"image", image:"data:...;base64,...", mimeType} generate block.
- Add FORMATS.COMMANDCODE to TARGETS_NEED_BASE64 so remote http(s) images are
  inlined by the existing SSRF-safe fetcher before translation.
- Write reasoning_effort inside params for targetFormat commandcode and pass
  low|medium|high|xhigh|max through unmapped; allow it in thinkingLevels.
- Provider-scoped capabilities for commandcode/cmc: vision except the CLI
  text-only denylist, thinkingFormat commandcode, so family patterns
  (deepseek-v4 -> thinkingFormat deepseek, vision false) no longer win.
- Quota Tracker: whoami + billing credits/subscriptions (credits vs plan cap,
  5h and weekly windows), labels from AI_PROVIDERS[].name.
This commit is contained in:
KhuatHieu
2026-09-16 20:16:32 +07:00
committed by decolua
parent 9300121366
commit 13b468b889
16 changed files with 529 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ import { fetchImageAsBase64, parseDataUri } from "./image.js";
const TARGETS_NEED_BASE64 = new Set([
FORMATS.GEMINI, FORMATS.GEMINI_CLI, FORMATS.VERTEX,
FORMATS.ANTIGRAVITY, FORMATS.OLLAMA, FORMATS.KIRO,
FORMATS.COMMANDCODE,
]);
function isRemoteUrl(url) {

View File

@@ -19,6 +19,7 @@ const FORMAT_TO_NATIVE = {
vertex: "gemini-budget",
antigravity: "gemini-budget",
kiro: "kiro",
commandcode: "commandcode",
};
// Strip a trailing thinking suffix "model(value)" → "model" (no-op when absent).
@@ -108,6 +109,7 @@ export const captureThinking = extractThinking;
const NATIVE_ONLY_FORMATS = new Set(["gemini-level", "gemini-budget", "claude-budget", "claude-adaptive", "kiro"]);
function resolveFormat(targetFormat, model, provider) {
if (targetFormat === "commandcode") return "commandcode";
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
if (providerFmt) return providerFmt;
const caps = getCapabilitiesForModel(provider, model);
@@ -223,6 +225,10 @@ function stripAll(body) {
delete body.output_config;
if (body.generationConfig) delete body.generationConfig.thinkingConfig;
if (body.request?.generationConfig) delete body.request.generationConfig.thinkingConfig;
if (body.params && typeof body.params === "object") {
delete body.params.reasoning_effort;
delete body.params.thinking;
}
}
// Apply unified thinking config to body in the resolved provider-native format.
@@ -336,6 +342,17 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) {
case "kiro":
// Kiro thinking handled via system-tag injection in openai-to-kiro.js; no body field here.
break;
case "commandcode": {
// Native CLI sends reasoning_effort inside params of the /alpha/generate envelope.
if (!body.params || typeof body.params !== "object") body.params = {};
if (none && canDisable) {
delete body.params.reasoning_effort;
break;
}
const level = toLevel(eff);
if (level) body.params.reasoning_effort = level;
break;
}
default:
break;
}

View File

@@ -5,6 +5,7 @@
* - params.system: STRING at top level (Anthropic-style; system messages NOT allowed in messages[])
* - params.messages[*].role ∈ {"user","assistant","tool"}
* - params.messages[*].content: Array of content blocks (NEVER a string)
* - image_url / image source → {type:"image", image:"data:...;base64,...", mimeType}
* - tool_use blocks (assistant): {type:"tool-call", toolCallId, toolName, input}
* - tool_result blocks (role=user): {type:"tool-result", toolCallId, toolName, output}
* - tools[*]: Anthropic plain {name, description, input_schema}
@@ -12,8 +13,9 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { randomUUID } from "crypto";
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
import { parseDataUri, encodeDataUri } from "../concerns/image.js";
function flattenText(content) {
if (content == null) return "";
@@ -29,6 +31,43 @@ function flattenText(content) {
return String(content);
}
function toNativeImageBlock(part) {
if (!part || typeof part !== "object") return null;
if (part.type === OPENAI_BLOCK.IMAGE_URL) {
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
const parsed = parseDataUri(url);
if (!parsed) return null;
return {
type: OPENAI_BLOCK.IMAGE,
image: encodeDataUri(parsed.mimeType, parsed.base64),
mimeType: parsed.mimeType,
};
}
if (part.type === OPENAI_BLOCK.IMAGE || part.type === CLAUDE_BLOCK.IMAGE) {
if (typeof part.image === "string" && part.image.startsWith("data:")) {
const parsed = parseDataUri(part.image);
return {
type: OPENAI_BLOCK.IMAGE,
image: part.image,
mimeType: part.mimeType || parsed?.mimeType || "image/png",
};
}
const source = part.source;
if (source?.type === "base64" && typeof source.data === "string") {
const mime = source.media_type || "image/png";
return {
type: OPENAI_BLOCK.IMAGE,
image: encodeDataUri(mime, source.data),
mimeType: mime,
};
}
}
return null;
}
function toContentBlocks(content) {
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }];
@@ -40,10 +79,12 @@ function toContentBlocks(content) {
} else if (part && typeof part === "object") {
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
} else if (part.type === OPENAI_BLOCK.IMAGE_URL || part.type === OPENAI_BLOCK.IMAGE) {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: "[image omitted]" });
} else if (typeof part.text === "string") {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
} else {
const image = toNativeImageBlock(part);
if (image) blocks.push(image);
else if (typeof part.text === "string") {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
}
}
}