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:
@@ -467,12 +467,66 @@ function refine(base, provider, model) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors Command Code CLI `isKnownTextOnlyModel` (no image input). New models
|
||||||
|
// default to vision; only this denylist stays text-only.
|
||||||
|
const COMMANDCODE_TEXT_ONLY = new Set([
|
||||||
|
"deepseek/deepseek-v4-pro",
|
||||||
|
"deepseek/deepseek-v4-flash",
|
||||||
|
"deepseek/deepseek-v4-flash-fast",
|
||||||
|
"zai-org/glm-5.3",
|
||||||
|
"zai-org/glm-5.2",
|
||||||
|
"zai-org/glm-5.2-fast",
|
||||||
|
"zai-org/glm-5.1",
|
||||||
|
"zai-org/glm-5",
|
||||||
|
"minimaxai/minimax-m2.7",
|
||||||
|
"minimax/minimax-m2.7-free",
|
||||||
|
"minimaxai/minimax-m2.5",
|
||||||
|
"xiaomi/mimo-v2.5-pro",
|
||||||
|
"qwen/qwen3.6-max-preview",
|
||||||
|
"qwen/qwen3.7-max",
|
||||||
|
"meituan/longcat-2.0:free",
|
||||||
|
"stepfun/step-3.5-flash",
|
||||||
|
"tencent/hy4-preview",
|
||||||
|
"tencent/hy3",
|
||||||
|
"tencent/hy3-paid",
|
||||||
|
"nvidia/nemotron-3-ultra-550b-a55b",
|
||||||
|
"poolside/laguna-s-2.1-free",
|
||||||
|
"inclusionai/ling-3.0-flash-free",
|
||||||
|
"inclusionai/ling-3.0-flash-sante:free",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isCommandCodeTextOnly(model) {
|
||||||
|
const key = String(model || "").toLowerCase();
|
||||||
|
if (COMMANDCODE_TEXT_ONLY.has(key)) return true;
|
||||||
|
for (const id of COMMANDCODE_TEXT_ONLY) {
|
||||||
|
const base = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
|
||||||
|
if (key === base || key.endsWith("/" + base)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
export function getCapabilitiesForModel(provider, model) {
|
export function getCapabilitiesForModel(provider, model) {
|
||||||
if (!model) return { ...DEFAULT_CAPABILITIES };
|
if (!model) return { ...DEFAULT_CAPABILITIES };
|
||||||
|
|
||||||
// Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7".
|
// Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7".
|
||||||
const baseModel = model.includes("/") ? model.split("/").pop() : model;
|
const baseModel = model.includes("/") ? model.split("/").pop() : model;
|
||||||
|
|
||||||
|
// CommandCode wire is /alpha/generate for every model. Family patterns
|
||||||
|
// (deepseek-v4 → thinkingFormat:deepseek, vision:false) must not win here.
|
||||||
|
if (provider === "commandcode" || provider === "cmc") {
|
||||||
|
const providerCaps = PROVIDER_CAPABILITIES.commandcode;
|
||||||
|
if (providerCaps?.[model]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[model] };
|
||||||
|
if (providerCaps?.[baseModel]) return { ...DEFAULT_CAPABILITIES, ...providerCaps[baseModel] };
|
||||||
|
return {
|
||||||
|
...DEFAULT_CAPABILITIES,
|
||||||
|
reasoning: true,
|
||||||
|
thinkingFormat: "commandcode",
|
||||||
|
thinkingEffortSupported: true,
|
||||||
|
vision: !isCommandCodeTextOnly(model),
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxOutput: 384000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Provider-specific override
|
// 1. Provider-specific override
|
||||||
if (provider) {
|
if (provider) {
|
||||||
const providerCaps = PROVIDER_CAPABILITIES[provider];
|
const providerCaps = PROVIDER_CAPABILITIES[provider];
|
||||||
|
|||||||
@@ -40,4 +40,8 @@ export default {
|
|||||||
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" },
|
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" },
|
||||||
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" },
|
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" },
|
||||||
],
|
],
|
||||||
|
features: {
|
||||||
|
usage: true,
|
||||||
|
usageApikey: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const FORMAT_LEVELS = {
|
|||||||
qwen: L.base,
|
qwen: L.base,
|
||||||
kimi: L.levelMax,
|
kimi: L.levelMax,
|
||||||
deepseek: L.hiMax,
|
deepseek: L.hiMax,
|
||||||
|
commandcode: ["none", "low", "medium", "high", "xhigh", "max"],
|
||||||
minimax: L.onOff,
|
minimax: L.onOff,
|
||||||
hunyuan: L.base,
|
hunyuan: L.base,
|
||||||
step: L.base,
|
step: L.base,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { getZedUsage } from "./usage/zed.js";
|
|||||||
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.js";
|
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.js";
|
||||||
import { resolveQoderCredentials } from "./qoderModels.js";
|
import { resolveQoderCredentials } from "./qoderModels.js";
|
||||||
import { getGlmUsage } from "./usage/glm.js";
|
import { getGlmUsage } from "./usage/glm.js";
|
||||||
|
import { getCommandCodeUsage } from "./usage/commandcode.js";
|
||||||
import {
|
import {
|
||||||
getIflowUsage,
|
getIflowUsage,
|
||||||
getOllamaUsage,
|
getOllamaUsage,
|
||||||
@@ -62,6 +63,7 @@ const USAGE_HANDLERS = {
|
|||||||
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
|
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
|
||||||
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||||
"xiaomi-mimo": (c) => getXiaomiMimoUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
"xiaomi-mimo": (c) => getXiaomiMimoUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||||
|
commandcode: (c) => getCommandCodeUsage(c.apiKey, c.proxyOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getUsageForProvider(connection, proxyOptions = null, options = {}) {
|
export async function getUsageForProvider(connection, proxyOptions = null, options = {}) {
|
||||||
|
|||||||
134
open-sse/services/usage/commandcode.js
Normal file
134
open-sse/services/usage/commandcode.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Command Code usage — billing credits + 5h/weekly rate windows.
|
||||||
|
* Mirrors ~/cc-usage.mjs: whoami → credits + subscriptions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||||
|
import { parseResetTime, toFiniteNumber } from "./shared.js";
|
||||||
|
|
||||||
|
const BASE = (process.env.COMMAND_CODE_API_BASE_URL || "https://api.commandcode.ai").replace(/\/$/, "");
|
||||||
|
|
||||||
|
const PLAN_NAMES = {
|
||||||
|
"individual-go": "Go",
|
||||||
|
"individual-goat": "GOAT",
|
||||||
|
"individual-pro": "Pro",
|
||||||
|
"individual-pro-v1": "Pro",
|
||||||
|
"individual-provider": "Provider",
|
||||||
|
"individual-max": "Max",
|
||||||
|
"individual-ultra": "Ultra",
|
||||||
|
"teams-pro": "Teams Pro",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAN_CAPS = {
|
||||||
|
"individual-go": 10,
|
||||||
|
"individual-goat": 70,
|
||||||
|
"individual-pro": 30,
|
||||||
|
"individual-pro-v1": 80,
|
||||||
|
"individual-provider": 15,
|
||||||
|
"individual-max": 150,
|
||||||
|
"individual-ultra": 300,
|
||||||
|
"teams-pro": 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
function qs(route, params) {
|
||||||
|
const s = new URLSearchParams(
|
||||||
|
Object.entries(params || {}).filter(([, v]) => v != null),
|
||||||
|
).toString();
|
||||||
|
return s ? `${route}?${s}` : route;
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowQuota(win) {
|
||||||
|
if (!win || typeof win !== "object") return null;
|
||||||
|
const used = toFiniteNumber(win.used, 0);
|
||||||
|
const total = toFiniteNumber(win.cap, 0);
|
||||||
|
if (total <= 0 && used <= 0) return null;
|
||||||
|
return {
|
||||||
|
used,
|
||||||
|
total,
|
||||||
|
remaining: Math.max(0, total - used),
|
||||||
|
unlimited: false,
|
||||||
|
resetAt: parseResetTime(win.resetAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|null|undefined} apiKey
|
||||||
|
* @param {object|null} proxyOptions
|
||||||
|
*/
|
||||||
|
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
||||||
|
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
||||||
|
return { message: "Command Code API key not available. Add a key to view usage." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${apiKey.trim()}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = async (route) => {
|
||||||
|
const response = await proxyAwareFetch(
|
||||||
|
BASE + route,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const whoamiRes = await get(qs("/alpha/whoami", { limits: "1" }));
|
||||||
|
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
||||||
|
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
|
||||||
|
}
|
||||||
|
if (!whoamiRes.ok) {
|
||||||
|
return { plan: "Command Code", message: `Command Code usage API error (${whoamiRes.status})` };
|
||||||
|
}
|
||||||
|
const whoami = await whoamiRes.json().catch(() => ({}));
|
||||||
|
const orgId = whoami?.org?.id ?? null;
|
||||||
|
|
||||||
|
const [creditsRes, subsRes] = await Promise.all([
|
||||||
|
get(qs("/alpha/billing/credits", { orgId })),
|
||||||
|
get(qs("/alpha/billing/subscriptions", { orgId })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (creditsRes.status === 401 || creditsRes.status === 403 || subsRes.status === 401 || subsRes.status === 403) {
|
||||||
|
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
|
||||||
|
}
|
||||||
|
if (!creditsRes.ok) {
|
||||||
|
return { plan: "Command Code", message: `Command Code credits API error (${creditsRes.status})` };
|
||||||
|
}
|
||||||
|
if (!subsRes.ok) {
|
||||||
|
return { plan: "Command Code", message: `Command Code subscriptions API error (${subsRes.status})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const creditsBody = await creditsRes.json().catch(() => ({}));
|
||||||
|
const subsBody = await subsRes.json().catch(() => ({}));
|
||||||
|
const planId = subsBody?.data?.planId ?? null;
|
||||||
|
const plan = (planId && PLAN_NAMES[planId]) || planId || "Command Code";
|
||||||
|
const cap = planId ? (PLAN_CAPS[planId] || 0) : 0;
|
||||||
|
const c = creditsBody?.credits || {};
|
||||||
|
const remaining =
|
||||||
|
toFiniteNumber(c.monthlyCredits, 0) +
|
||||||
|
toFiniteNumber(c.purchasedCredits, 0) +
|
||||||
|
toFiniteNumber(c.freeCredits, 0);
|
||||||
|
const used = cap > 0 ? Math.max(0, cap - remaining) : 0;
|
||||||
|
const total = cap > 0 ? cap : remaining;
|
||||||
|
|
||||||
|
const quotas = {};
|
||||||
|
quotas.Credits = {
|
||||||
|
used,
|
||||||
|
total,
|
||||||
|
remaining,
|
||||||
|
unlimited: cap <= 0,
|
||||||
|
resetAt: parseResetTime(subsBody?.data?.currentPeriodEnd),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fiveHour = windowQuota(creditsBody?.windowLimits?.fiveHour);
|
||||||
|
if (fiveHour) quotas["Session (5h)"] = fiveHour;
|
||||||
|
const weekly = windowQuota(creditsBody?.windowLimits?.weekly);
|
||||||
|
if (weekly) quotas.Weekly = weekly;
|
||||||
|
|
||||||
|
return { plan, quotas };
|
||||||
|
} catch (error) {
|
||||||
|
return { message: `Command Code error: ${error.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { fetchImageAsBase64, parseDataUri } from "./image.js";
|
|||||||
const TARGETS_NEED_BASE64 = new Set([
|
const TARGETS_NEED_BASE64 = new Set([
|
||||||
FORMATS.GEMINI, FORMATS.GEMINI_CLI, FORMATS.VERTEX,
|
FORMATS.GEMINI, FORMATS.GEMINI_CLI, FORMATS.VERTEX,
|
||||||
FORMATS.ANTIGRAVITY, FORMATS.OLLAMA, FORMATS.KIRO,
|
FORMATS.ANTIGRAVITY, FORMATS.OLLAMA, FORMATS.KIRO,
|
||||||
|
FORMATS.COMMANDCODE,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isRemoteUrl(url) {
|
function isRemoteUrl(url) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const FORMAT_TO_NATIVE = {
|
|||||||
vertex: "gemini-budget",
|
vertex: "gemini-budget",
|
||||||
antigravity: "gemini-budget",
|
antigravity: "gemini-budget",
|
||||||
kiro: "kiro",
|
kiro: "kiro",
|
||||||
|
commandcode: "commandcode",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strip a trailing thinking suffix "model(value)" → "model" (no-op when absent).
|
// 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"]);
|
const NATIVE_ONLY_FORMATS = new Set(["gemini-level", "gemini-budget", "claude-budget", "claude-adaptive", "kiro"]);
|
||||||
|
|
||||||
function resolveFormat(targetFormat, model, provider) {
|
function resolveFormat(targetFormat, model, provider) {
|
||||||
|
if (targetFormat === "commandcode") return "commandcode";
|
||||||
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
|
const providerFmt = provider ? PROVIDERS[provider]?.thinkingFormat : null;
|
||||||
if (providerFmt) return providerFmt;
|
if (providerFmt) return providerFmt;
|
||||||
const caps = getCapabilitiesForModel(provider, model);
|
const caps = getCapabilitiesForModel(provider, model);
|
||||||
@@ -223,6 +225,10 @@ function stripAll(body) {
|
|||||||
delete body.output_config;
|
delete body.output_config;
|
||||||
if (body.generationConfig) delete body.generationConfig.thinkingConfig;
|
if (body.generationConfig) delete body.generationConfig.thinkingConfig;
|
||||||
if (body.request?.generationConfig) delete body.request.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.
|
// 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":
|
case "kiro":
|
||||||
// Kiro thinking handled via system-tag injection in openai-to-kiro.js; no body field here.
|
// Kiro thinking handled via system-tag injection in openai-to-kiro.js; no body field here.
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* - params.system: STRING at top level (Anthropic-style; system messages NOT allowed in messages[])
|
* - params.system: STRING at top level (Anthropic-style; system messages NOT allowed in messages[])
|
||||||
* - params.messages[*].role ∈ {"user","assistant","tool"}
|
* - params.messages[*].role ∈ {"user","assistant","tool"}
|
||||||
* - params.messages[*].content: Array of content blocks (NEVER a string)
|
* - 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_use blocks (assistant): {type:"tool-call", toolCallId, toolName, input}
|
||||||
* - tool_result blocks (role=user): {type:"tool-result", toolCallId, toolName, output}
|
* - tool_result blocks (role=user): {type:"tool-result", toolCallId, toolName, output}
|
||||||
* - tools[*]: Anthropic plain {name, description, input_schema}
|
* - tools[*]: Anthropic plain {name, description, input_schema}
|
||||||
@@ -12,8 +13,9 @@
|
|||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.js";
|
import { FORMATS } from "../formats.js";
|
||||||
import { randomUUID } from "crypto";
|
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 { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
|
||||||
|
import { parseDataUri, encodeDataUri } from "../concerns/image.js";
|
||||||
|
|
||||||
function flattenText(content) {
|
function flattenText(content) {
|
||||||
if (content == null) return "";
|
if (content == null) return "";
|
||||||
@@ -29,6 +31,43 @@ function flattenText(content) {
|
|||||||
return String(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) {
|
function toContentBlocks(content) {
|
||||||
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
||||||
if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }];
|
if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }];
|
||||||
@@ -40,13 +79,15 @@ function toContentBlocks(content) {
|
|||||||
} else if (part && typeof part === "object") {
|
} else if (part && typeof part === "object") {
|
||||||
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
|
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
|
||||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||||
} else if (part.type === OPENAI_BLOCK.IMAGE_URL || part.type === OPENAI_BLOCK.IMAGE) {
|
} else {
|
||||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: "[image omitted]" });
|
const image = toNativeImageBlock(part);
|
||||||
} else if (typeof part.text === "string") {
|
if (image) blocks.push(image);
|
||||||
|
else if (typeof part.text === "string") {
|
||||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
return blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
||||||
}
|
}
|
||||||
return [{ type: OPENAI_BLOCK.TEXT, text: String(content) }];
|
return [{ type: OPENAI_BLOCK.TEXT, text: String(content) }];
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
import Card from "@/shared/components/Card";
|
import Card from "@/shared/components/Card";
|
||||||
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
|
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
|
||||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
import { USAGE_SUPPORTED_PROVIDERS, AI_PROVIDERS } from "@/shared/constants/providers";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
|
|
||||||
// Maps the stored providerSpecificData.authMethod to a human label for Kiro.
|
// Maps the stored providerSpecificData.authMethod to a human label for Kiro.
|
||||||
@@ -100,6 +100,10 @@ function getCodexResetCreditCount(quota) {
|
|||||||
return Number.isFinite(count) ? Math.max(0, count) : 0;
|
return Number.isFinite(count) ? Math.max(0, count) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerLabel(providerId) {
|
||||||
|
return AI_PROVIDERS[providerId]?.name || providerId;
|
||||||
|
}
|
||||||
|
|
||||||
function formatCreditDate(value) {
|
function formatCreditDate(value) {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -767,7 +771,7 @@ export default function ProviderLimits() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectedProviderLabel =
|
const selectedProviderLabel =
|
||||||
providerFilter === "all" ? "All providers" : providerFilter;
|
providerFilter === "all" ? "All providers" : providerLabel(providerFilter);
|
||||||
const hasEligibleConnections = totals.eligibleConnections > 0;
|
const hasEligibleConnections = totals.eligibleConnections > 0;
|
||||||
const hasVisibleConnections = sortedConnections.length > 0;
|
const hasVisibleConnections = sortedConnections.length > 0;
|
||||||
const emptyState = getConnectionsEmptyMessage(
|
const emptyState = getConnectionsEmptyMessage(
|
||||||
@@ -844,7 +848,7 @@ export default function ProviderLimits() {
|
|||||||
fallbackText={providerFilter.slice(0, 2).toUpperCase()}
|
fallbackText={providerFilter.slice(0, 2).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="truncate capitalize hidden lg:inline">
|
<span className="truncate hidden lg:inline">
|
||||||
{selectedProviderLabel}
|
{selectedProviderLabel}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -905,8 +909,8 @@ export default function ProviderLimits() {
|
|||||||
className="size-6 rounded-md object-contain"
|
className="size-6 rounded-md object-contain"
|
||||||
fallbackText={provider.slice(0, 2).toUpperCase()}
|
fallbackText={provider.slice(0, 2).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
<span className="font-medium capitalize">
|
<span className="font-medium">
|
||||||
{provider}
|
{providerLabel(provider)}
|
||||||
</span>
|
</span>
|
||||||
{providerFilter === provider && (
|
{providerFilter === provider && (
|
||||||
<span className="material-symbols-outlined ml-auto text-[20px]">
|
<span className="material-symbols-outlined ml-auto text-[20px]">
|
||||||
@@ -1079,8 +1083,8 @@ export default function ProviderLimits() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-sm font-semibold text-text-primary capitalize truncate">
|
<h3 className="text-sm font-semibold text-text-primary truncate">
|
||||||
{conn.provider}
|
{providerLabel(conn.provider)}
|
||||||
</h3>
|
</h3>
|
||||||
{getConnectionLabel(conn) ? (
|
{getConnectionLabel(conn) ? (
|
||||||
<p className="text-xs text-text-muted truncate">
|
<p className="text-xs text-text-muted truncate">
|
||||||
|
|||||||
@@ -62,15 +62,18 @@ describe("OpenAI → CommandCode", () => {
|
|||||||
expect(Object.keys(call.input).length, "arguments silently dropped to {}").toBeGreaterThan(0);
|
expect(Object.keys(call.input).length, "arguments silently dropped to {}").toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// openai-to-commandcode.js:41-42 — image becomes "[image omitted]"
|
it("image content is preserved as native CommandCode image blocks", () => {
|
||||||
// KNOWN BUG
|
|
||||||
it.fails("image content is preserved", () => {
|
|
||||||
const out = O2CC({
|
const out = O2CC({
|
||||||
messages: [{ role: "user", content: [
|
messages: [{ role: "user", content: [
|
||||||
{ type: "text", text: "look" },
|
{ type: "text", text: "look" },
|
||||||
{ type: "image_url", image_url: { url: "data:image/png;base64,BBBB" } },
|
{ type: "image_url", image_url: { url: "data:image/png;base64,BBBB" } },
|
||||||
] }],
|
] }],
|
||||||
});
|
});
|
||||||
expect(JSON.stringify(out), "image omitted").toContain("BBBB");
|
expect(JSON.stringify(out)).toContain("BBBB");
|
||||||
|
expect(JSON.stringify(out)).not.toContain("[image omitted]");
|
||||||
|
expect(out.params.messages[0].content).toEqual([
|
||||||
|
{ type: "text", text: "look" },
|
||||||
|
{ type: "image", image: "data:image/png;base64,BBBB", mimeType: "image/png" },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -250,6 +250,29 @@ describe("applyThinking per provider format", () => {
|
|||||||
const out = apply("gemini-cli", "gemini-3.5-flash-lite", { reasoning_effort: "medium" }, "gemini-cli");
|
const out = apply("gemini-cli", "gemini-3.5-flash-lite", { reasoning_effort: "medium" }, "gemini-cli");
|
||||||
expect(out.generationConfig.thinkingConfig.thinkingLevel).toBe("medium");
|
expect(out.generationConfig.thinkingConfig.thinkingLevel).toBe("medium");
|
||||||
});
|
});
|
||||||
|
it("commandcode envelope writes params.reasoning_effort, not wrapper fields", () => {
|
||||||
|
const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", {
|
||||||
|
params: { model: "deepseek/deepseek-v4.1-flash", messages: [] },
|
||||||
|
reasoning_effort: "high",
|
||||||
|
}, "commandcode");
|
||||||
|
expect(out.params.reasoning_effort).toBe("high");
|
||||||
|
expect(out.reasoning_effort).toBeUndefined();
|
||||||
|
expect(out.thinking).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("commandcode preserves low effort instead of remapping to high", () => {
|
||||||
|
const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", {
|
||||||
|
params: { messages: [] },
|
||||||
|
reasoning_effort: "low",
|
||||||
|
}, "commandcode");
|
||||||
|
expect(out.params.reasoning_effort).toBe("low");
|
||||||
|
});
|
||||||
|
it("commandcode preserves max effort", () => {
|
||||||
|
const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", {
|
||||||
|
params: { messages: [] },
|
||||||
|
reasoning_effort: "max",
|
||||||
|
}, "commandcode");
|
||||||
|
expect(out.params.reasoning_effort).toBe("max");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractReasoningText (response shapes)", () => {
|
describe("extractReasoningText (response shapes)", () => {
|
||||||
|
|||||||
@@ -73,4 +73,26 @@ describe("getCapabilitiesForModel", () => {
|
|||||||
maxOutput: 128000,
|
maxOutput: 128000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("CommandCode v4.1-flash is vision + effort capable", () => {
|
||||||
|
expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4.1-flash")).toMatchObject({
|
||||||
|
vision: true,
|
||||||
|
reasoning: true,
|
||||||
|
thinkingFormat: "commandcode",
|
||||||
|
thinkingEffortSupported: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("CommandCode MiniMax-M3 is vision capable", () => {
|
||||||
|
expect(getCapabilitiesForModel("commandcode", "MiniMaxAI/MiniMax-M3").vision).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("CommandCode text-only DeepSeek V4 Flash stays non-vision", () => {
|
||||||
|
expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4-flash").vision).toBe(false);
|
||||||
|
expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4-flash")).toMatchObject({
|
||||||
|
reasoning: true,
|
||||||
|
thinkingFormat: "commandcode",
|
||||||
|
thinkingEffortSupported: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
135
tests/unit/commandcode-usage.test.js
Normal file
135
tests/unit/commandcode-usage.test.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||||
|
proxyAwareFetch: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
|
||||||
|
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||||
|
import {
|
||||||
|
USAGE_SUPPORTED_PROVIDERS,
|
||||||
|
USAGE_APIKEY_PROVIDERS,
|
||||||
|
} from "../../src/shared/constants/providers.js";
|
||||||
|
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||||
|
|
||||||
|
const BASE = "https://api.commandcode.ai";
|
||||||
|
|
||||||
|
function jsonResponse(body, status = 200) {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const WHOAMI = {
|
||||||
|
user: { name: "Hieu", email: "hieu@example.com" },
|
||||||
|
org: { id: "org_1", name: "personal" },
|
||||||
|
};
|
||||||
|
const CREDITS = {
|
||||||
|
credits: { monthlyCredits: 12.5, purchasedCredits: 1, freeCredits: 0.5 },
|
||||||
|
windowLimits: {
|
||||||
|
fiveHour: { used: 2, cap: 10, resetAt: Date.now() + 3_600_000, exceeded: false },
|
||||||
|
weekly: { used: 20, cap: 70, resetAt: Date.now() + 86_400_000, exceeded: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const SUBS = {
|
||||||
|
data: {
|
||||||
|
planId: "individual-goat",
|
||||||
|
currentPeriodStart: "2026-09-01T00:00:00.000Z",
|
||||||
|
currentPeriodEnd: "2026-10-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function mockHappyPath() {
|
||||||
|
proxyAwareFetch.mockImplementation(async (url) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (u.includes("/alpha/whoami")) return jsonResponse(WHOAMI);
|
||||||
|
if (u.includes("/alpha/billing/credits")) return jsonResponse(CREDITS);
|
||||||
|
if (u.includes("/alpha/billing/subscriptions")) return jsonResponse(SUBS);
|
||||||
|
return jsonResponse({ error: "unexpected " + u }, 404);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("commandcode registry usage flags", () => {
|
||||||
|
it("is listed for apikey quota dashboard", () => {
|
||||||
|
expect(USAGE_SUPPORTED_PROVIDERS).toContain("commandcode");
|
||||||
|
expect(USAGE_APIKEY_PROVIDERS).toContain("commandcode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUsageForProvider(commandcode)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a message when apiKey is missing", async () => {
|
||||||
|
const usage = await getUsageForProvider({ provider: "commandcode" });
|
||||||
|
expect(usage.message).toMatch(/api key/i);
|
||||||
|
expect(proxyAwareFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GETs whoami, credits, and subscriptions with Bearer apiKey", async () => {
|
||||||
|
mockHappyPath();
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.message).toBeUndefined();
|
||||||
|
expect(usage.plan).toBe("GOAT");
|
||||||
|
const urls = proxyAwareFetch.mock.calls.map(([url]) => String(url));
|
||||||
|
expect(urls.some((u) => u.startsWith(`${BASE}/alpha/whoami`))).toBe(true);
|
||||||
|
expect(urls.some((u) => u.includes("/alpha/billing/credits") && u.includes("orgId=org_1"))).toBe(true);
|
||||||
|
expect(urls.some((u) => u.includes("/alpha/billing/subscriptions") && u.includes("orgId=org_1"))).toBe(true);
|
||||||
|
expect(proxyAwareFetch.mock.calls[0][1].headers.Authorization).toBe("Bearer user_test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps remaining credits vs plan cap and rate windows", async () => {
|
||||||
|
mockHappyPath();
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// remaining = 12.5 + 1 + 0.5 = 14; cap GOAT = 70; used = 56
|
||||||
|
expect(usage.quotas.Credits).toMatchObject({
|
||||||
|
used: 56,
|
||||||
|
total: 70,
|
||||||
|
unlimited: false,
|
||||||
|
});
|
||||||
|
expect(usage.quotas["Session (5h)"]).toMatchObject({
|
||||||
|
used: 2,
|
||||||
|
total: 10,
|
||||||
|
unlimited: false,
|
||||||
|
});
|
||||||
|
expect(usage.quotas.Weekly).toMatchObject({
|
||||||
|
used: 20,
|
||||||
|
total: 70,
|
||||||
|
});
|
||||||
|
expect(new Date(usage.quotas.Credits.resetAt).toISOString()).toBe("2026-10-01T00:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an auth message on 401", async () => {
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "bad",
|
||||||
|
});
|
||||||
|
expect(usage.message).toMatch(/auth|key|login/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseQuotaData(commandcode)", () => {
|
||||||
|
it("forwards used/total/resetAt for the dashboard table", () => {
|
||||||
|
const rows = parseQuotaData("commandcode", {
|
||||||
|
plan: "GOAT",
|
||||||
|
quotas: {
|
||||||
|
Credits: { used: 56, total: 70, resetAt: "2026-10-01T00:00:00.000Z" },
|
||||||
|
"Session (5h)": { used: 2, total: 10, resetAt: "2026-09-16T10:00:00.000Z" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(rows).toHaveLength(2);
|
||||||
|
expect(rows[0]).toMatchObject({ name: "Credits", used: 56, total: 70 });
|
||||||
|
expect(rows[1]).toMatchObject({ name: "Session (5h)", used: 2, total: 10 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -179,3 +179,57 @@ describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
|||||||
expect(out.params.tools).toBeUndefined();
|
expect(out.params.tools).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("openaiToCommandCodeRequest — native image blocks", () => {
|
||||||
|
const PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
||||||
|
const DATA_URI = `data:image/png;base64,${PNG_B64}`;
|
||||||
|
|
||||||
|
it("maps OpenAI image_url data URI to CommandCode {type:image,image,mimeType}", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(MODEL, {
|
||||||
|
messages: [{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "what color?" },
|
||||||
|
{ type: "image_url", image_url: { url: DATA_URI } },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
expect(out.params.messages[0].content).toEqual([
|
||||||
|
{ type: "text", text: "what color?" },
|
||||||
|
{ type: "image", image: DATA_URI, mimeType: "image/png" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps Claude/OpenAI base64 image source to a data-URI image block", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(MODEL, {
|
||||||
|
messages: [{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "image", source: { type: "base64", media_type: "image/png", data: PNG_B64 } },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
expect(out.params.messages[0].content).toEqual([
|
||||||
|
{ type: "image", image: DATA_URI, mimeType: "image/png" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not stub dropped images as [image omitted]", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(MODEL, {
|
||||||
|
messages: [{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "see this" },
|
||||||
|
{ type: "image_url", image_url: { url: DATA_URI } },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
const texts = out.params.messages[0].content
|
||||||
|
.filter((b) => b.type === "text")
|
||||||
|
.map((b) => b.text);
|
||||||
|
expect(texts).not.toContain("[image omitted]");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -55,4 +55,21 @@ describe("prefetchRemoteImages", () => {
|
|||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
expect(body.messages[0].content[0].source.type).toBe("base64");
|
expect(body.messages[0].content[0].source.type).toBe("base64");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("openai source -> commandcode target: converts remote URL to base64", async () => {
|
||||||
|
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] };
|
||||||
|
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.COMMANDCODE);
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect(body.messages[0].content[0].image_url.url.startsWith("data:image/png;base64,")).toBe(true);
|
||||||
|
expect(fetchImageAsBase64).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("claude source -> commandcode target: source.url -> base64", async () => {
|
||||||
|
const body = { messages: [{ role: "user", content: [
|
||||||
|
{ type: "image", source: { type: "url", url: "https://x/a.png" } },
|
||||||
|
] }] };
|
||||||
|
const n = await prefetchRemoteImages(body, FORMATS.CLAUDE, FORMATS.COMMANDCODE);
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect(body.messages[0].content[0].source.type).toBe("base64");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const SUPPORTED = [
|
|||||||
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
|
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
|
||||||
"qoder", "iflow", "ollama", "glm", "glm-cn",
|
"qoder", "iflow", "ollama", "glm", "glm-cn",
|
||||||
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi",
|
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi",
|
||||||
"deepseek", "opencode-go", "zed",
|
"deepseek", "opencode-go", "zed", "commandcode",
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("usage dispatch", () => {
|
describe("usage dispatch", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user