diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 8ad55314..177db4a7 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -467,12 +467,66 @@ function refine(base, provider, model) { 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) { if (!model) return { ...DEFAULT_CAPABILITIES }; // Canonical exact lookup strips vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7". 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 if (provider) { const providerCaps = PROVIDER_CAPABILITIES[provider]; diff --git a/open-sse/providers/registry/commandcode.js b/open-sse/providers/registry/commandcode.js index 3b21fbbc..f59aac04 100644 --- a/open-sse/providers/registry/commandcode.js +++ b/open-sse/providers/registry/commandcode.js @@ -40,4 +40,8 @@ export default { { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus" }, { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash" }, ], + features: { + usage: true, + usageApikey: true, + }, }; diff --git a/open-sse/providers/thinkingLevels.js b/open-sse/providers/thinkingLevels.js index 404fa75e..0b77e4b4 100644 --- a/open-sse/providers/thinkingLevels.js +++ b/open-sse/providers/thinkingLevels.js @@ -26,6 +26,7 @@ const FORMAT_LEVELS = { qwen: L.base, kimi: L.levelMax, deepseek: L.hiMax, + commandcode: ["none", "low", "medium", "high", "xhigh", "max"], minimax: L.onOff, hunyuan: L.base, step: L.base, diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index d7868760..cca5d861 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -20,6 +20,7 @@ import { getZedUsage } from "./usage/zed.js"; import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.js"; import { resolveQoderCredentials } from "./qoderModels.js"; import { getGlmUsage } from "./usage/glm.js"; +import { getCommandCodeUsage } from "./usage/commandcode.js"; import { getIflowUsage, getOllamaUsage, @@ -62,6 +63,7 @@ const USAGE_HANDLERS = { groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions), zed: (c) => getZedUsage(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 = {}) { diff --git a/open-sse/services/usage/commandcode.js b/open-sse/services/usage/commandcode.js new file mode 100644 index 00000000..7c62480c --- /dev/null +++ b/open-sse/services/usage/commandcode.js @@ -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}` }; + } +} diff --git a/open-sse/translator/concerns/prefetch.js b/open-sse/translator/concerns/prefetch.js index 5055be3c..4d606ebd 100644 --- a/open-sse/translator/concerns/prefetch.js +++ b/open-sse/translator/concerns/prefetch.js @@ -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) { diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index e86dbef7..001259e3 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -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; } diff --git a/open-sse/translator/request/openai-to-commandcode.js b/open-sse/translator/request/openai-to-commandcode.js index 9825048b..194078ae 100644 --- a/open-sse/translator/request/openai-to-commandcode.js +++ b/open-sse/translator/request/openai-to-commandcode.js @@ -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 }); + } } } } diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js index 568c4808..0a508265 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js @@ -40,7 +40,7 @@ import { } from "./utils"; import Card from "@/shared/components/Card"; 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"; // 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; } +function providerLabel(providerId) { + return AI_PROVIDERS[providerId]?.name || providerId; +} + function formatCreditDate(value) { if (!value) return "N/A"; const date = new Date(value); @@ -767,7 +771,7 @@ export default function ProviderLimits() { }; const selectedProviderLabel = - providerFilter === "all" ? "All providers" : providerFilter; + providerFilter === "all" ? "All providers" : providerLabel(providerFilter); const hasEligibleConnections = totals.eligibleConnections > 0; const hasVisibleConnections = sortedConnections.length > 0; const emptyState = getConnectionsEmptyMessage( @@ -844,7 +848,7 @@ export default function ProviderLimits() { fallbackText={providerFilter.slice(0, 2).toUpperCase()} /> )} - + {selectedProviderLabel} @@ -905,8 +909,8 @@ export default function ProviderLimits() { className="size-6 rounded-md object-contain" fallbackText={provider.slice(0, 2).toUpperCase()} /> - - {provider} + + {providerLabel(provider)} {providerFilter === provider && ( @@ -1079,8 +1083,8 @@ export default function ProviderLimits() { />
-

- {conn.provider} +

+ {providerLabel(conn.provider)}

{getConnectionLabel(conn) ? (

diff --git a/tests/translator/bugs-gemini-cursor-commandcode.test.js b/tests/translator/bugs-gemini-cursor-commandcode.test.js index 4b74c60e..8483bfc1 100644 --- a/tests/translator/bugs-gemini-cursor-commandcode.test.js +++ b/tests/translator/bugs-gemini-cursor-commandcode.test.js @@ -62,15 +62,18 @@ describe("OpenAI → CommandCode", () => { expect(Object.keys(call.input).length, "arguments silently dropped to {}").toBeGreaterThan(0); }); - // openai-to-commandcode.js:41-42 — image becomes "[image omitted]" - // KNOWN BUG - it.fails("image content is preserved", () => { + it("image content is preserved as native CommandCode image blocks", () => { const out = O2CC({ messages: [{ role: "user", content: [ { type: "text", text: "look" }, { 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" }, + ]); }); }); diff --git a/tests/translator/thinking-unified.test.js b/tests/translator/thinking-unified.test.js index fd8f9d28..f04b18bd 100644 --- a/tests/translator/thinking-unified.test.js +++ b/tests/translator/thinking-unified.test.js @@ -250,6 +250,29 @@ describe("applyThinking per provider format", () => { const out = apply("gemini-cli", "gemini-3.5-flash-lite", { reasoning_effort: "medium" }, "gemini-cli"); 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)", () => { diff --git a/tests/unit/capabilities.test.js b/tests/unit/capabilities.test.js index 3482a2c8..cde8e9b4 100644 --- a/tests/unit/capabilities.test.js +++ b/tests/unit/capabilities.test.js @@ -73,4 +73,26 @@ describe("getCapabilitiesForModel", () => { 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, + }); + }); }); diff --git a/tests/unit/commandcode-usage.test.js b/tests/unit/commandcode-usage.test.js new file mode 100644 index 00000000..5f38dc47 --- /dev/null +++ b/tests/unit/commandcode-usage.test.js @@ -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 }); + }); +}); diff --git a/tests/unit/openai-to-commandcode.test.js b/tests/unit/openai-to-commandcode.test.js index 7f12dc85..0a441dd7 100644 --- a/tests/unit/openai-to-commandcode.test.js +++ b/tests/unit/openai-to-commandcode.test.js @@ -179,3 +179,57 @@ describe("openaiToCommandCodeRequest — tools schema conversion", () => { 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]"); + }); +}); diff --git a/tests/unit/prefetch-images.test.js b/tests/unit/prefetch-images.test.js index 2289697b..0d4c0db3 100644 --- a/tests/unit/prefetch-images.test.js +++ b/tests/unit/prefetch-images.test.js @@ -55,4 +55,21 @@ describe("prefetchRemoteImages", () => { expect(n).toBe(1); 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"); + }); }); diff --git a/tests/unit/usage-dispatch.test.js b/tests/unit/usage-dispatch.test.js index 84a5f469..ead63892 100644 --- a/tests/unit/usage-dispatch.test.js +++ b/tests/unit/usage-dispatch.test.js @@ -16,7 +16,7 @@ const SUPPORTED = [ "github", "gemini-cli", "antigravity", "claude", "codex", "kiro", "qoder", "iflow", "ollama", "glm", "glm-cn", "minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi", - "deepseek", "opencode-go", "zed", + "deepseek", "opencode-go", "zed", "commandcode", ]; describe("usage dispatch", () => {