feat(commandcode): align quota usage with the official CLI /usage flow
Rewrites services/usage/commandcode.js to mirror the command-code CLI: whoami resolves the org id, credits+subscriptions report the 5-hour/weekly windows and plan, then usage/summary is queried with since=currentPeriodStart. Endpoints are read from the provider registry usage block instead of hardcoded constants. Tests updated to cover the new request order and response shapes.
This commit is contained in:
@@ -1,134 +1,207 @@
|
|||||||
/**
|
/**
|
||||||
* Command Code usage — billing credits + 5h/weekly rate windows.
|
* CommandCode usage handler
|
||||||
* Mirrors ~/cc-usage.mjs: whoami → credits + subscriptions.
|
*
|
||||||
|
* Mirrors the official command-code CLI /usage command: it calls the alpha API
|
||||||
|
* to surface the 5-hour + weekly usage windows, the subscription plan, and the
|
||||||
|
* credits consumed in the current billing period.
|
||||||
|
*
|
||||||
|
* GET /alpha/whoami → org.id (org-scoped billing; null for personal)
|
||||||
|
* GET /alpha/billing/credits → { credits: { monthlyCredits, purchasedCredits,
|
||||||
|
* freeCredits }, windowLimits: { fiveHour, weekly } }
|
||||||
|
* GET /alpha/billing/subscriptions → { data: { planId, currentPeriodStart, ... } }
|
||||||
|
* GET /alpha/usage/summary?since= → period token/cost totals
|
||||||
|
*
|
||||||
|
* The CLI fetches whoami first (for orgId), then credits + subscription in
|
||||||
|
* parallel, then the summary with since = currentPeriodStart. We keep the same
|
||||||
|
* order/dependencies: window limits live on credits, and the plan period start
|
||||||
|
* determines the summary window.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||||
import { parseResetTime, toFiniteNumber } from "./shared.js";
|
import { U, parseResetTime } from "./shared.js";
|
||||||
|
|
||||||
const BASE = (process.env.COMMAND_CODE_API_BASE_URL || "https://api.commandcode.ai").replace(/\/$/, "");
|
const USAGE = U("commandcode");
|
||||||
|
const BASE = USAGE.baseUrl || "https://api.commandcode.ai";
|
||||||
|
const WHOAMI_URL = BASE + (USAGE.whoamiUrl || "/alpha/whoami");
|
||||||
|
const CREDITS_URL = BASE + (USAGE.creditsUrl || "/alpha/billing/credits");
|
||||||
|
const SUBSCRIPTIONS_URL =
|
||||||
|
BASE + (USAGE.subscriptionsUrl || "/alpha/billing/subscriptions");
|
||||||
|
const SUMMARY_URL = BASE + (USAGE.summaryUrl || "/alpha/usage/summary");
|
||||||
|
|
||||||
const PLAN_NAMES = {
|
function buildHeaders(token) {
|
||||||
"individual-go": "Go",
|
return {
|
||||||
"individual-goat": "GOAT",
|
Authorization: `Bearer ${token}`,
|
||||||
"individual-pro": "Pro",
|
Accept: "application/json",
|
||||||
"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) {
|
/** Build a normalized quota row. `unit` is "$" — the API reports currency credits. */
|
||||||
if (!win || typeof win !== "object") return null;
|
function makeQuota({ used, total, resetAt, unlimited = false, unit = "$" }) {
|
||||||
const used = toFiniteNumber(win.used, 0);
|
const safeTotal = Math.max(0, Number(total) || 0);
|
||||||
const total = toFiniteNumber(win.cap, 0);
|
const safeUsed = Math.max(0, Number(used) || 0);
|
||||||
if (total <= 0 && used <= 0) return null;
|
if (unlimited || safeTotal === 0) {
|
||||||
return {
|
return {
|
||||||
used,
|
used: safeUsed,
|
||||||
total,
|
total: 0,
|
||||||
remaining: Math.max(0, total - used),
|
remainingPercentage: unlimited ? 100 : 0,
|
||||||
|
resetAt: resetAt || null,
|
||||||
|
unit,
|
||||||
|
unlimited: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const remaining = Math.max(0, safeTotal - safeUsed);
|
||||||
|
const remainingPercentage = (remaining / safeTotal) * 100;
|
||||||
|
return {
|
||||||
|
used: safeUsed,
|
||||||
|
total: safeTotal,
|
||||||
|
remainingPercentage,
|
||||||
|
resetAt: resetAt || null,
|
||||||
|
unit,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
resetAt: parseResetTime(win.resetAt),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string|null|undefined} apiKey
|
* @param {string} apiKey - commandcode API key (user_...)
|
||||||
* @param {object|null} proxyOptions
|
* @param {object|null} proxyOptions
|
||||||
*/
|
*/
|
||||||
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
||||||
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
if (!apiKey) {
|
||||||
return { message: "Command Code API key not available. Add a key to view usage." };
|
return { message: "CommandCode credential not available." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = buildHeaders(apiKey);
|
||||||
Authorization: `Bearer ${apiKey.trim()}`,
|
|
||||||
Accept: "application/json",
|
|
||||||
};
|
|
||||||
|
|
||||||
const get = async (route) => {
|
try {
|
||||||
const response = await proxyAwareFetch(
|
// whoami resolves the org id (billing is org-scoped; null for personal).
|
||||||
BASE + route,
|
const whoamiRes = await proxyAwareFetch(
|
||||||
|
WHOAMI_URL,
|
||||||
{ method: "GET", headers },
|
{ method: "GET", headers },
|
||||||
proxyOptions,
|
proxyOptions,
|
||||||
);
|
);
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const whoamiRes = await get(qs("/alpha/whoami", { limits: "1" }));
|
|
||||||
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
||||||
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
|
return { message: "CommandCode credential invalid or expired." };
|
||||||
}
|
}
|
||||||
if (!whoamiRes.ok) {
|
if (!whoamiRes.ok) {
|
||||||
return { plan: "Command Code", message: `Command Code usage API error (${whoamiRes.status})` };
|
return { message: `CommandCode whoami API error (${whoamiRes.status}).` };
|
||||||
}
|
}
|
||||||
const whoami = await whoamiRes.json().catch(() => ({}));
|
const whoami = await whoamiRes.json().catch(() => null);
|
||||||
const orgId = whoami?.org?.id ?? null;
|
const orgId = whoami?.org?.id ?? null;
|
||||||
|
|
||||||
|
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
||||||
|
|
||||||
const [creditsRes, subsRes] = await Promise.all([
|
const [creditsRes, subsRes] = await Promise.all([
|
||||||
get(qs("/alpha/billing/credits", { orgId })),
|
proxyAwareFetch(
|
||||||
get(qs("/alpha/billing/subscriptions", { orgId })),
|
CREDITS_URL + orgQuery,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
),
|
||||||
|
proxyAwareFetch(
|
||||||
|
SUBSCRIPTIONS_URL + orgQuery,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (creditsRes.status === 401 || creditsRes.status === 403 || subsRes.status === 401 || subsRes.status === 403) {
|
if (
|
||||||
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
|
creditsRes.status === 401 ||
|
||||||
|
creditsRes.status === 403 ||
|
||||||
|
subsRes.status === 401 ||
|
||||||
|
subsRes.status === 403
|
||||||
|
) {
|
||||||
|
return { message: "CommandCode credential invalid or expired." };
|
||||||
}
|
}
|
||||||
if (!creditsRes.ok) {
|
if (!creditsRes.ok) {
|
||||||
return { plan: "Command Code", message: `Command Code credits API error (${creditsRes.status})` };
|
return {
|
||||||
}
|
message: `CommandCode 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 credits = await creditsRes.json().catch(() => null);
|
||||||
const subsBody = await subsRes.json().catch(() => ({}));
|
const subs = await subsRes.json().catch(() => null);
|
||||||
const planId = subsBody?.data?.planId ?? null;
|
|
||||||
const plan = (planId && PLAN_NAMES[planId]) || planId || "Command Code";
|
const subData = subs?.data;
|
||||||
const cap = planId ? (PLAN_CAPS[planId] || 0) : 0;
|
const planId = subData?.planId ?? null;
|
||||||
const c = creditsBody?.credits || {};
|
const periodStart = subData?.currentPeriodStart ?? null;
|
||||||
const remaining =
|
|
||||||
toFiniteNumber(c.monthlyCredits, 0) +
|
// Summary needs `since`; the CLI falls back to first-of-month when the
|
||||||
toFiniteNumber(c.purchasedCredits, 0) +
|
// subscription period start is unavailable.
|
||||||
toFiniteNumber(c.freeCredits, 0);
|
const since = periodStart || firstOfMonth();
|
||||||
const used = cap > 0 ? Math.max(0, cap - remaining) : 0;
|
const summaryRes = await proxyAwareFetch(
|
||||||
const total = cap > 0 ? cap : remaining;
|
`${SUMMARY_URL}?since=${encodeURIComponent(since)}`,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
);
|
||||||
|
const summary = summaryRes.ok
|
||||||
|
? await summaryRes.json().catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const quotas = {};
|
const quotas = {};
|
||||||
quotas.Credits = {
|
const windowLimits = credits?.windowLimits || {};
|
||||||
used,
|
|
||||||
total,
|
const fiveHour = windowLimits.fiveHour;
|
||||||
remaining,
|
if (fiveHour && Number(fiveHour.cap) > 0) {
|
||||||
unlimited: cap <= 0,
|
quotas["5-hour window"] = makeQuota({
|
||||||
resetAt: parseResetTime(subsBody?.data?.currentPeriodEnd),
|
used: fiveHour.used,
|
||||||
|
total: fiveHour.cap,
|
||||||
|
resetAt: parseResetTime(fiveHour.resetAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekly = windowLimits.weekly;
|
||||||
|
if (weekly && Number(weekly.cap) > 0) {
|
||||||
|
quotas["Weekly window"] = makeQuota({
|
||||||
|
used: weekly.used,
|
||||||
|
total: weekly.cap,
|
||||||
|
resetAt: parseResetTime(weekly.resetAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The credits API reports remaining balances (monthly/purchased/free),
|
||||||
|
// not a total. The official CLI renders the monthly line as
|
||||||
|
// `used = summary.totalCost`, `total = totalCost + remaining` — i.e.
|
||||||
|
// the plan ceiling is the sum of what was consumed and what is left.
|
||||||
|
const monthlyUsed =
|
||||||
|
typeof summary?.totalCredits === "number"
|
||||||
|
? summary.totalCredits
|
||||||
|
: typeof summary?.totalCost === "number"
|
||||||
|
? summary.totalCost
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const creditsObj = credits?.credits || {};
|
||||||
|
const remaining =
|
||||||
|
Math.max(0, Number(creditsObj.monthlyCredits) || 0) +
|
||||||
|
Math.max(0, Number(creditsObj.purchasedCredits) || 0) +
|
||||||
|
Math.max(0, Number(creditsObj.freeCredits) || 0);
|
||||||
|
const monthlyTotal = monthlyUsed + remaining;
|
||||||
|
|
||||||
|
if (monthlyTotal > 0 || monthlyUsed > 0) {
|
||||||
|
quotas["Monthly credits"] = makeQuota({
|
||||||
|
used: monthlyUsed,
|
||||||
|
total: monthlyTotal,
|
||||||
|
resetAt: periodStart ? undefined : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(quotas).length === 0) {
|
||||||
|
return {
|
||||||
|
plan: planId || "CommandCode",
|
||||||
|
message: "CommandCode connected, but no quota was reported.",
|
||||||
|
quotas: {},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const fiveHour = windowQuota(creditsBody?.windowLimits?.fiveHour);
|
return {
|
||||||
if (fiveHour) quotas["Session (5h)"] = fiveHour;
|
plan: planId || "CommandCode",
|
||||||
const weekly = windowQuota(creditsBody?.windowLimits?.weekly);
|
quotas,
|
||||||
if (weekly) quotas.Weekly = weekly;
|
periodBasis: summary?.periodBasis || "billing-period",
|
||||||
|
};
|
||||||
return { plan, quotas };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { message: `Command Code error: ${error.message}` };
|
return { message: `CommandCode usage error: ${error.message}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstOfMonth() {
|
||||||
|
const now = new Date();
|
||||||
|
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||||
|
|
||||||
const BASE = "https://api.commandcode.ai";
|
const BASE = "https://api.commandcode.ai";
|
||||||
|
const WHOAMI_URL = `${BASE}/alpha/whoami`;
|
||||||
|
const CREDITS_URL = `${BASE}/alpha/billing/credits`;
|
||||||
|
const SUBS_URL = `${BASE}/alpha/billing/subscriptions`;
|
||||||
|
const SUMMARY_URL = `${BASE}/alpha/usage/summary`;
|
||||||
|
|
||||||
function jsonResponse(body, status = 200) {
|
function jsonResponse(body, status = 200) {
|
||||||
return new Response(JSON.stringify(body), {
|
return new Response(JSON.stringify(body), {
|
||||||
@@ -21,34 +25,40 @@ function jsonResponse(body, status = 200) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const WHOAMI = {
|
const WHOAMI = { success: true, user: { id: "u1" }, org: null };
|
||||||
user: { name: "Hieu", email: "hieu@example.com" },
|
|
||||||
org: { id: "org_1", name: "personal" },
|
|
||||||
};
|
|
||||||
const CREDITS = {
|
const CREDITS = {
|
||||||
credits: { monthlyCredits: 12.5, purchasedCredits: 1, freeCredits: 0.5 },
|
credits: {
|
||||||
|
belowThreshold: false,
|
||||||
|
creditThreshold: 0,
|
||||||
|
monthlyCredits: 9.9,
|
||||||
|
purchasedCredits: 0,
|
||||||
|
freeCredits: 0,
|
||||||
|
},
|
||||||
windowLimits: {
|
windowLimits: {
|
||||||
fiveHour: { used: 2, cap: 10, resetAt: Date.now() + 3_600_000, exceeded: false },
|
limited: true,
|
||||||
weekly: { used: 20, cap: 70, resetAt: Date.now() + 86_400_000, exceeded: false },
|
exceeded: null,
|
||||||
|
fiveHour: { used: 0.05, cap: 3, exceeded: false, resetAt: 1785812386064 },
|
||||||
|
weekly: { used: 0.1, cap: 6, exceeded: false, resetAt: 1786379982640 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const SUBS = {
|
const SUBS = {
|
||||||
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
planId: "individual-goat",
|
id: "sub_1",
|
||||||
currentPeriodStart: "2026-09-01T00:00:00.000Z",
|
status: "active",
|
||||||
currentPeriodEnd: "2026-10-01T00:00:00.000Z",
|
orgId: null,
|
||||||
|
planId: "individual-go",
|
||||||
|
currentPeriodStart: "2026-08-03T16:38:16.000Z",
|
||||||
|
currentPeriodEnd: "2026-09-03T16:38:16.000Z",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const SUMMARY = {
|
||||||
function mockHappyPath() {
|
totalCount: 61,
|
||||||
proxyAwareFetch.mockImplementation(async (url) => {
|
totalCost: 0.1,
|
||||||
const u = String(url);
|
totalCredits: 0.1,
|
||||||
if (u.includes("/alpha/whoami")) return jsonResponse(WHOAMI);
|
totalMonthlyCredits: 0.1,
|
||||||
if (u.includes("/alpha/billing/credits")) return jsonResponse(CREDITS);
|
periodBasis: "billing-period",
|
||||||
if (u.includes("/alpha/billing/subscriptions")) return jsonResponse(SUBS);
|
};
|
||||||
return jsonResponse({ error: "unexpected " + u }, 404);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("commandcode registry usage flags", () => {
|
describe("commandcode registry usage flags", () => {
|
||||||
it("is listed for apikey quota dashboard", () => {
|
it("is listed for apikey quota dashboard", () => {
|
||||||
@@ -62,74 +72,146 @@ describe("getUsageForProvider(commandcode)", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a message when apiKey is missing", async () => {
|
it("fetches whoami → credits+subs → summary and maps windows + credits", async () => {
|
||||||
const usage = await getUsageForProvider({ provider: "commandcode" });
|
proxyAwareFetch
|
||||||
expect(usage.message).toMatch(/api key/i);
|
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||||
expect(proxyAwareFetch).not.toHaveBeenCalled();
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
});
|
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
it("GETs whoami, credits, and subscriptions with Bearer apiKey", async () => {
|
|
||||||
mockHappyPath();
|
|
||||||
const usage = await getUsageForProvider({
|
const usage = await getUsageForProvider({
|
||||||
provider: "commandcode",
|
provider: "commandcode",
|
||||||
apiKey: "user_test",
|
apiKey: "user_cc_test",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(usage.message).toBeUndefined();
|
expect(usage.message).toBeUndefined();
|
||||||
expect(usage.plan).toBe("GOAT");
|
expect(usage.plan).toBe("individual-go");
|
||||||
const urls = proxyAwareFetch.mock.calls.map(([url]) => String(url));
|
expect(usage.periodBasis).toBe("billing-period");
|
||||||
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 () => {
|
expect(proxyAwareFetch).toHaveBeenCalledTimes(4);
|
||||||
mockHappyPath();
|
const [whoamiUrl, whoamiOpts] = proxyAwareFetch.mock.calls[0];
|
||||||
const usage = await getUsageForProvider({
|
expect(whoamiUrl).toBe(WHOAMI_URL);
|
||||||
provider: "commandcode",
|
expect(whoamiOpts.headers.Authorization).toBe("Bearer user_cc_test");
|
||||||
apiKey: "user_test",
|
|
||||||
});
|
|
||||||
|
|
||||||
// remaining = 12.5 + 1 + 0.5 = 14; cap GOAT = 70; used = 56
|
// No org → credits/subscriptions called without orgId query
|
||||||
expect(usage.quotas.Credits).toMatchObject({
|
const creditsCall = proxyAwareFetch.mock.calls[1][0];
|
||||||
used: 56,
|
expect(creditsCall).toBe(CREDITS_URL);
|
||||||
total: 70,
|
|
||||||
unlimited: false,
|
// Summary uses currentPeriodStart as `since`
|
||||||
|
const summaryCall = proxyAwareFetch.mock.calls[3][0];
|
||||||
|
expect(summaryCall).toBe(
|
||||||
|
`${SUMMARY_URL}?since=${encodeURIComponent("2026-08-03T16:38:16.000Z")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(usage.quotas["5-hour window"]).toMatchObject({
|
||||||
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
resetAt: new Date(1785812386064).toISOString(),
|
||||||
});
|
});
|
||||||
expect(usage.quotas["Session (5h)"]).toMatchObject({
|
expect(usage.quotas["Weekly window"]).toMatchObject({
|
||||||
used: 2,
|
used: 0.1,
|
||||||
|
total: 6,
|
||||||
|
resetAt: new Date(1786379982640).toISOString(),
|
||||||
|
});
|
||||||
|
// monthlyCredits/purchasedCredits/freeCredits are remaining balances;
|
||||||
|
// total = consumed (summary.totalCredits) + remaining, matching the CLI.
|
||||||
|
expect(usage.quotas["Monthly credits"]).toMatchObject({
|
||||||
|
used: 0.1,
|
||||||
total: 10,
|
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 () => {
|
it("adds orgId query when whoami returns an org", async () => {
|
||||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
|
proxyAwareFetch
|
||||||
const usage = await getUsageForProvider({
|
.mockResolvedValueOnce(
|
||||||
|
jsonResponse({ success: true, org: { id: "org_1" } }),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
|
await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_cc_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(proxyAwareFetch.mock.calls[1][0]).toBe(`${CREDITS_URL}?orgId=org_1`);
|
||||||
|
expect(proxyAwareFetch.mock.calls[2][0]).toBe(`${SUBS_URL}?orgId=org_1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to first-of-month since when subscription has no period start", async () => {
|
||||||
|
proxyAwareFetch
|
||||||
|
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
jsonResponse({ success: true, data: { planId: "individual-go" } }),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
|
await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_cc_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
const since = new URL(proxyAwareFetch.mock.calls[3][0]).searchParams.get(
|
||||||
|
"since",
|
||||||
|
);
|
||||||
|
// firstOfMonth() is local-time based; assert the local date is the 1st.
|
||||||
|
const localDate = new Date(since);
|
||||||
|
expect(localDate.getDate()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns message on missing key / 401 / non-ok whoami", async () => {
|
||||||
|
const missing = await getUsageForProvider({ provider: "commandcode" });
|
||||||
|
expect(missing.message).toMatch(/credential/i);
|
||||||
|
expect(proxyAwareFetch).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "no" }, 401));
|
||||||
|
const auth = await getUsageForProvider({
|
||||||
provider: "commandcode",
|
provider: "commandcode",
|
||||||
apiKey: "bad",
|
apiKey: "bad",
|
||||||
});
|
});
|
||||||
expect(usage.message).toMatch(/auth|key|login/i);
|
expect(auth.message).toMatch(/invalid|expired/i);
|
||||||
|
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "x" }, 500));
|
||||||
|
const err = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "bad",
|
||||||
|
});
|
||||||
|
expect(err.message).toMatch(/whoami/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parseQuotaData(commandcode)", () => {
|
describe("parseQuotaData(commandcode)", () => {
|
||||||
it("forwards used/total/resetAt for the dashboard table", () => {
|
it("forwards remainingPercentage + unit for window/credit rows", () => {
|
||||||
const rows = parseQuotaData("commandcode", {
|
const rows = parseQuotaData("commandcode", {
|
||||||
plan: "GOAT",
|
plan: "individual-go",
|
||||||
quotas: {
|
quotas: {
|
||||||
Credits: { used: 56, total: 70, resetAt: "2026-10-01T00:00:00.000Z" },
|
"5-hour window": {
|
||||||
"Session (5h)": { used: 2, total: 10, resetAt: "2026-09-16T10:00:00.000Z" },
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
remainingPercentage: 98.33,
|
||||||
|
resetAt: "2026-08-03T22:59:46.064Z",
|
||||||
|
unit: "$",
|
||||||
|
},
|
||||||
|
"Monthly credits": {
|
||||||
|
used: 0.1,
|
||||||
|
total: 10,
|
||||||
|
remainingPercentage: 99,
|
||||||
|
resetAt: null,
|
||||||
|
unit: "$",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(rows).toHaveLength(2);
|
expect(rows[0]).toMatchObject({
|
||||||
expect(rows[0]).toMatchObject({ name: "Credits", used: 56, total: 70 });
|
name: "5-hour window",
|
||||||
expect(rows[1]).toMatchObject({ name: "Session (5h)", used: 2, total: 10 });
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
expect(rows[1]).toMatchObject({
|
||||||
|
name: "Monthly credits",
|
||||||
|
used: 0.1,
|
||||||
|
total: 10,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ const MODEL = "moonshotai/Kimi-K2.6";
|
|||||||
|
|
||||||
describe("openaiToCommandCodeRequest — basic envelope", () => {
|
describe("openaiToCommandCodeRequest — basic envelope", () => {
|
||||||
it("returns the expected top-level envelope shape", () => {
|
it("returns the expected top-level envelope shape", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hi" }],
|
messages: [{ role: "user", content: "hi" }],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
expect(out).toHaveProperty("threadId");
|
expect(out).toHaveProperty("threadId");
|
||||||
expect(out).toHaveProperty("memory");
|
expect(out).toHaveProperty("memory");
|
||||||
@@ -30,12 +34,16 @@ describe("openaiToCommandCodeRequest — basic envelope", () => {
|
|||||||
|
|
||||||
describe("openaiToCommandCodeRequest — system handling", () => {
|
describe("openaiToCommandCodeRequest — system handling", () => {
|
||||||
it("hoists system messages to params.system (string), not messages[]", () => {
|
it("hoists system messages to params.system (string), not messages[]", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "You are concise." },
|
{ role: "system", content: "You are concise." },
|
||||||
{ role: "user", content: "hi" },
|
{ role: "user", content: "hi" },
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
expect(typeof out.params.system).toBe("string");
|
expect(typeof out.params.system).toBe("string");
|
||||||
expect(out.params.system).toBe("You are concise.");
|
expect(out.params.system).toBe("You are concise.");
|
||||||
@@ -44,30 +52,149 @@ describe("openaiToCommandCodeRequest — system handling", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("joins multiple system messages with blank line", () => {
|
it("joins multiple system messages with blank line", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: "A" },
|
{ role: "system", content: "A" },
|
||||||
{ role: "system", content: "B" },
|
{ role: "system", content: "B" },
|
||||||
{ role: "user", content: "hi" },
|
{ role: "user", content: "hi" },
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
expect(out.params.system).toBe("A\n\nB");
|
expect(out.params.system).toBe("A\n\nB");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits params.system when no system messages", () => {
|
it("omits params.system when no system messages", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hi" }],
|
messages: [{ role: "user", content: "hi" }],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(out.params.system).toBeUndefined();
|
expect(out.params.system).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("openaiToCommandCodeRequest — vision / image blocks", () => {
|
||||||
|
const PNG =
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
||||||
|
|
||||||
|
it('maps OpenAI image_url (data URI) → {type:"image", image:"data:..."}', () => {
|
||||||
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "What color?" },
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: `data:image/png;base64,${PNG}` },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const blocks = out.params.messages[0].content;
|
||||||
|
expect(blocks[0]).toEqual({ type: "text", text: "What color?" });
|
||||||
|
expect(blocks[1]).toEqual({
|
||||||
|
type: "image",
|
||||||
|
image: `data:image/png;base64,${PNG}`,
|
||||||
|
mimeType: "image/png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps Claude-style image block (source.base64) → data URI with media_type", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: "image/jpeg",
|
||||||
|
data: "AAAA",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(out.params.messages[0].content[0]).toEqual({
|
||||||
|
type: "image",
|
||||||
|
image: "data:image/jpeg;base64,AAAA",
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes raw URL through when image_url is a remote http(s) URL", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: "https://example.com/a.png" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(out.params.messages[0].content[0]).toEqual({
|
||||||
|
type: "image",
|
||||||
|
image: "https://example.com/a.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips image block when no usable image source", () => {
|
||||||
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "image_url", image_url: { url: "" } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const blocks = out.params.messages[0].content;
|
||||||
|
expect(blocks.every((b) => b.type !== "image")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("openaiToCommandCodeRequest — content shape", () => {
|
describe("openaiToCommandCodeRequest — content shape", () => {
|
||||||
it("MUST always emit content as Array (never string) for user", () => {
|
it("MUST always emit content as Array (never string) for user", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hello" }],
|
messages: [{ role: "user", content: "hello" }],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const u = out.params.messages[0];
|
const u = out.params.messages[0];
|
||||||
expect(Array.isArray(u.content)).toBe(true);
|
expect(Array.isArray(u.content)).toBe(true);
|
||||||
@@ -75,12 +202,16 @@ describe("openaiToCommandCodeRequest — content shape", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("MUST always emit content as Array for assistant", () => {
|
it("MUST always emit content as Array for assistant", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "user", content: "a" },
|
{ role: "user", content: "a" },
|
||||||
{ role: "assistant", content: "b" },
|
{ role: "assistant", content: "b" },
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
const a = out.params.messages[1];
|
const a = out.params.messages[1];
|
||||||
expect(Array.isArray(a.content)).toBe(true);
|
expect(Array.isArray(a.content)).toBe(true);
|
||||||
expect(a.content[0]).toEqual({ type: "text", text: "b" });
|
expect(a.content[0]).toEqual({ type: "text", text: "b" });
|
||||||
@@ -88,20 +219,33 @@ describe("openaiToCommandCodeRequest — content shape", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("openaiToCommandCodeRequest — tool role / tool-result (AI SDK)", () => {
|
describe("openaiToCommandCodeRequest — tool role / tool-result (AI SDK)", () => {
|
||||||
it("converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}", () => {
|
it('converts role:"tool" to role:"tool" with tool-result block; output is {type:"text",value}', () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "user", content: "run X" },
|
{ role: "user", content: "run X" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: null,
|
content: null,
|
||||||
tool_calls: [
|
tool_calls: [
|
||||||
{ id: "call_1", type: "function", function: { name: "do_x", arguments: "{\"a\":1}" } },
|
{
|
||||||
|
id: "call_1",
|
||||||
|
type: "function",
|
||||||
|
function: { name: "do_x", arguments: '{"a":1}' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ role: "tool", tool_call_id: "call_1", name: "do_x", content: "RESULT_OK" },
|
{
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: "call_1",
|
||||||
|
name: "do_x",
|
||||||
|
content: "RESULT_OK",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const toolMsg = out.params.messages[out.params.messages.length - 1];
|
const toolMsg = out.params.messages[out.params.messages.length - 1];
|
||||||
expect(toolMsg.role).toBe("tool");
|
expect(toolMsg.role).toBe("tool");
|
||||||
@@ -115,18 +259,26 @@ describe("openaiToCommandCodeRequest — tool role / tool-result (AI SDK)", () =
|
|||||||
|
|
||||||
describe("openaiToCommandCodeRequest — assistant tool_calls / tool-call", () => {
|
describe("openaiToCommandCodeRequest — assistant tool_calls / tool-call", () => {
|
||||||
it("converts assistant.tool_calls[] into content blocks of type tool-call", () => {
|
it("converts assistant.tool_calls[] into content blocks of type tool-call", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "user", content: "go" },
|
{ role: "user", content: "go" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: null,
|
content: null,
|
||||||
tool_calls: [
|
tool_calls: [
|
||||||
{ id: "call_42", type: "function", function: { name: "search", arguments: "{\"q\":\"hi\"}" } },
|
{
|
||||||
|
id: "call_42",
|
||||||
|
type: "function",
|
||||||
|
function: { name: "search", arguments: '{"q":"hi"}' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const asst = out.params.messages[1];
|
const asst = out.params.messages[1];
|
||||||
expect(asst.role).toBe("assistant");
|
expect(asst.role).toBe("assistant");
|
||||||
@@ -139,8 +291,10 @@ describe("openaiToCommandCodeRequest — assistant tool_calls / tool-call", () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
||||||
it("converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}", () => {
|
it('converts OpenAI {type:"function", function:{...}} to Anthropic plain {name, input_schema}', () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hi" }],
|
messages: [{ role: "user", content: "hi" }],
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
@@ -148,11 +302,17 @@ describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
|||||||
function: {
|
function: {
|
||||||
name: "weather",
|
name: "weather",
|
||||||
description: "Get weather",
|
description: "Get weather",
|
||||||
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: { city: { type: "string" } },
|
||||||
|
required: ["city"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const t = out.params.tools[0];
|
const t = out.params.tools[0];
|
||||||
expect(t.name).toBe("weather");
|
expect(t.name).toBe("weather");
|
||||||
@@ -163,73 +323,34 @@ describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves description on converted tool", () => {
|
it("preserves description on converted tool", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hi" }],
|
messages: [{ role: "user", content: "hi" }],
|
||||||
tools: [
|
tools: [
|
||||||
{ type: "function", function: { name: "ping", description: "Ping the server", parameters: { type: "object" } } },
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "ping",
|
||||||
|
description: "Ping the server",
|
||||||
|
parameters: { type: "object" },
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(out.params.tools[0].description).toBe("Ping the server");
|
expect(out.params.tools[0].description).toBe("Ping the server");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not include tools field when input has none", () => {
|
it("does not include tools field when input has none", () => {
|
||||||
const out = openaiToCommandCodeRequest(MODEL, {
|
const out = openaiToCommandCodeRequest(
|
||||||
|
MODEL,
|
||||||
|
{
|
||||||
messages: [{ role: "user", content: "hi" }],
|
messages: [{ role: "user", content: "hi" }],
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
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]");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user