merge: sync upstream v0.5.81 into MIBP fork

# Conflicts:
#	.gitignore
#	Dockerfile
#	open-sse/handlers/chatCore.js
#	open-sse/providers/registry/cline.js
#	open-sse/providers/registry/index.js
#	open-sse/services/usage.js
#	open-sse/utils/streamHandler.js
#	package.json
#	src/app/(dashboard)/dashboard/profile/page.js
#	src/app/(dashboard)/dashboard/providers/[id]/page.js
This commit is contained in:
MUH. IQRAM BAHRING committed 2026-09-19 11:35:34 +08:00
commit 0ac771bad8
208 files changed
+13062 -977

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
// Regression: an unmatched 4xx (a request-scoped failure) used to hit the
// transient-cooldown default, which locked the account for 30s and — with a
// single connection — answered every other request in that window with a copy of
// the first error. A 400 "maximum context length" from one session therefore
// looked like the same failure in unrelated sessions.
import { describe, expect, it } from "vitest";
import { checkFallbackError } from "../../open-sse/services/accountFallback.js";
describe("checkFallbackError — request-scoped vs account-scoped failures", () => {
it("does not cool the account down for a 400 caused by the request", () => {
const result = checkFallbackError(400, JSON.stringify({
error: {
message: "This model's maximum context length is 1048576 tokens. However, you requested 1186139 tokens",
type: "invalid_request_error",
},
}));
expect(result).toEqual({ shouldFallback: false, cooldownMs: 0 });
});
it("still falls back for account-scoped statuses", () => {
for (const status of [401, 402, 403, 404, 429]) {
expect(checkFallbackError(status, "nope").shouldFallback).toBe(true);
}
});
it("still honours rate-limit / quota wording on any 4xx", () => {
expect(checkFallbackError(400, "rate limit reached").shouldFallback).toBe(true);
expect(checkFallbackError(422, "quota exceeded").shouldFallback).toBe(true);
});
it("keeps the transient cooldown for unmatched server errors", () => {
const result = checkFallbackError(503, "upstream exploded");
expect(result.shouldFallback).toBe(true);
expect(result.cooldownMs).toBeGreaterThan(0);
});
});
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js";
import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.js";
const HEADER = "x-anthropic-billing-header: cc_version=2.1.275.f15; cc_entrypoint=cli;";
function systemTextSentToAntigravity(systemContent) {
// OpenAI-format client (e.g. a proxy converting Claude Code to /v1/chat/completions).
const body = openaiToAntigravityRequest("gemini-3.8-flash-tiered", {
messages: [
{ role: "system", content: systemContent },
{ role: "user", content: "hi" },
],
}, true);
const finalBody = new AntigravityExecutor().transformRequest("gemini-3.8-flash-tiered", body, true, {});
return finalBody.request.systemInstruction.parts.map((p) => p.text).join("\n");
}
describe("Antigravity strips the Claude Code billing header from system prompts", () => {
it("removes the header line prepended by Claude Code", () => {
const text = systemTextSentToAntigravity(`${HEADER}\n\nYou are Claude Code, Anthropic's official CLI for Claude.`);
expect(text).not.toContain("x-anthropic-billing-header");
expect(text).toContain("You are Claude Code, Anthropic's official CLI for Claude.");
});
it("removes the header when it is not the first line", () => {
const text = systemTextSentToAntigravity(`Some preamble\n${HEADER}\nRest of prompt`);
expect(text).not.toContain("x-anthropic-billing-header");
expect(text).toContain("Some preamble");
expect(text).toContain("Rest of prompt");
});
it("leaves prompts without the header untouched", () => {
const text = systemTextSentToAntigravity("You are a helpful assistant.");
expect(text).toContain("You are a helpful assistant.");
});
});
@@ -4,7 +4,7 @@ const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }
: {
models: {
"gemini-3.6-flash-high": {
@@ -4,7 +4,7 @@ const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }
: {
models: {
"gemini-3.7-flash-high": {
@@ -4,7 +4,7 @@ const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }
: {
models: {
"gemini-3.8-flash-high": {
@@ -0,0 +1,104 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Keep the signature store in RAM only; the SQLite kv layer is not under test here.
vi.mock("@/lib/db/helpers/kvStore.js", () => ({
makeKv: () => ({
get: async () => null,
set: async () => {},
remove: async () => {},
getAll: async () => ({}),
}),
}));
const {
storeGeminiThoughtSignature,
getGeminiThoughtSignatureSync,
signatureFamily,
} = await import("../../open-sse/services/thoughtSignatureStore.js");
const { openaiToAntigravityRequest } = await import("../../open-sse/translator/request/openai-to-gemini.js");
const { geminiToOpenAIResponse } = await import("../../open-sse/translator/response/gemini-to-openai.js");
const { DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } = await import("../../open-sse/config/defaultThinkingSignature.js");
let n = 0;
const uid = (p) => `${p}_${Date.now()}_${n++}`;
function toolHistory(callId) {
return {
messages: [
{ role: "user", content: "list files" },
{ role: "assistant", content: null, tool_calls: [{ id: callId, type: "function", function: { name: "ls", arguments: "{}" } }] },
{ role: "tool", tool_call_id: callId, content: "a.txt" },
],
};
}
function functionCallSignatures(req) {
return req.request.contents.flatMap((c) => c.parts || []).filter((p) => p.functionCall).map((p) => p.thoughtSignature);
}
describe("antigravity thought signatures are scoped to the model family", () => {
beforeEach(() => { n++; });
it("classifies model families", () => {
expect(signatureFamily("claude-opus-4-6-thinking")).toBe("claude");
expect(signatureFamily("gemini-3.8-flash-tiered")).toBe("gemini");
expect(signatureFamily("gpt-oss-120b-medium")).toBe("gpt-oss-120b-medium");
expect(signatureFamily(null)).toBe(null);
});
it("does not return a Claude signature for a Gemini target (and vice versa)", () => {
const claudeCall = uid("toolu");
const geminiCall = uid("call");
storeGeminiThoughtSignature(claudeCall, "CLAUDE_SIG", "sess", "claude-opus-4-6-thinking");
storeGeminiThoughtSignature(geminiCall, "GEMINI_SIG", "sess", "gemini-3.8-flash-tiered");
expect(getGeminiThoughtSignatureSync(claudeCall, "sess", "gemini-3.8-flash")).toBe(null);
expect(getGeminiThoughtSignatureSync(claudeCall, "sess", "claude-opus-4-6-thinking")).toBe("CLAUDE_SIG");
expect(getGeminiThoughtSignatureSync(geminiCall, "sess", "gemini-3.7-flash")).toBe("GEMINI_SIG");
expect(getGeminiThoughtSignatureSync(geminiCall, null, "claude-sonnet-4-6")).toBe(null);
});
it("keeps old behaviour for untagged entries and untargeted lookups", () => {
const call = uid("call");
storeGeminiThoughtSignature(call, "LEGACY_SIG", "sess");
expect(getGeminiThoughtSignatureSync(call, "sess", "gemini-3.8-flash")).toBe("LEGACY_SIG");
const tagged = uid("toolu");
storeGeminiThoughtSignature(tagged, "CLAUDE_SIG", "sess", "claude-opus-4-6-thinking");
expect(getGeminiThoughtSignatureSync(tagged, "sess")).toBe("CLAUDE_SIG");
});
it("records the producing model from the Gemini response stream", () => {
const call = uid("toolu_vrtx");
const state = { model: "claude-opus-4-6-thinking", sessionId: null, toolNameMap: null };
geminiToOpenAIResponse({
response: {
responseId: "r1",
candidates: [{ content: { role: "model", parts: [{ functionCall: { id: call, name: "ls", args: {} }, thoughtSignature: "CLAUDE_SIG" }] } }],
},
}, state);
expect(getGeminiThoughtSignatureSync(call, null, "gemini-3.8-flash-tiered")).toBe(null);
expect(getGeminiThoughtSignatureSync(call, null, "claude-opus-4-6-thinking")).toBe("CLAUDE_SIG");
});
it("switching Claude -> Gemini mid-conversation sends the default signature, not Claude's", () => {
const call = uid("toolu_vrtx");
storeGeminiThoughtSignature(call, "CLAUDE_SIG", null, "claude-opus-4-6-thinking");
const req = openaiToAntigravityRequest("gemini-3.8-flash-tiered", toolHistory(call), true);
expect(functionCallSignatures(req)).toEqual([DEFAULT_THINKING_GEMINI_CLI_SIGNATURE]);
});
it("switching Gemini -> Claude mid-conversation does not replay Gemini's signature", () => {
const call = uid("call");
storeGeminiThoughtSignature(call, "GEMINI_SIG", null, "gemini-3.8-flash-tiered");
const req = openaiToAntigravityRequest("claude-opus-4-6-thinking", toolHistory(call), true);
expect(functionCallSignatures(req)).not.toContain("GEMINI_SIG");
});
it("same model family still reuses the cached signature", () => {
const call = uid("call");
storeGeminiThoughtSignature(call, "GEMINI_SIG", null, "gemini-3.8-flash-tiered");
const req = openaiToAntigravityRequest("gemini-3.8-flash-tiered", toolHistory(call), true);
expect(functionCallSignatures(req)).toEqual(["GEMINI_SIG"]);
});
});
+6 -3
View File
@@ -4,8 +4,10 @@ const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
: { models: {} },
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }
: url.includes(":retrieveUserQuotaSummary")
? { groups: [] }
: { models: {} },
text: async () => "{}",
}));
@@ -21,7 +23,8 @@ describe("Antigravity usage headers", () => {
await getAntigravityUsage("access-token", {});
expect(proxyAwareFetch).toHaveBeenCalledTimes(2);
// loadCodeAssist + fetchAvailableModels + retrieveUserQuotaSummary
expect(proxyAwareFetch).toHaveBeenCalledTimes(3);
for (const [, options] of proxyAwareFetch.mock.calls) {
expect(options.headers["User-Agent"]).toBe("antigravity/ide/2.11.0 darwin/arm64");
expect(options.headers).not.toHaveProperty("x-request-source");
@@ -0,0 +1,113 @@
import { describe, it, expect } from "vitest";
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
describe("Antigravity dashboard normalization with weekly quotas", () => {
const data = {
quotas: {
"gemini-pro-agent": {
displayName: "Gemini 3.1 Pro (High)",
used: 200,
total: 1000,
resetAt: "2026-09-08T00:00:00Z",
remainingPercentage: 80,
},
"claude-opus-4-6-thinking": {
displayName: "Claude Opus 4.6 (Thinking)",
used: 100,
total: 1000,
resetAt: "2026-09-08T00:00:00Z",
remainingPercentage: 90,
},
gemini_weekly: {
displayName: "Gemini (Weekly)",
used: 250,
total: 1000,
resetAt: "2026-09-15T00:00:00Z",
remainingPercentage: 75,
},
claude_gpt_weekly: {
displayName: "Claude & GPT (Weekly)",
used: 500,
total: 1000,
resetAt: "2026-09-14T00:00:00Z",
remainingPercentage: 50,
},
},
};
it("includes weekly rows with correct display names", () => {
const quotas = parseQuotaData("antigravity", data);
const names = quotas.map((q) => q.name);
expect(names).toContain("Gemini (Flash / Pro)");
expect(names).toContain("Claude (Sonnet / Opus)");
expect(names).toContain("Gemini (Weekly)");
expect(names).toContain("Claude & GPT (Weekly)");
});
it("uses stable modelKey for weekly rows", () => {
const quotas = parseQuotaData("antigravity", data);
const keys = quotas.map((q) => q.modelKey);
expect(keys).toContain("gemini_weekly");
expect(keys).toContain("claude_gpt_weekly");
});
it("weekly rows carry correct quota values", () => {
const quotas = parseQuotaData("antigravity", data);
const geminiWeekly = quotas.find((q) => q.modelKey === "gemini_weekly");
const claudeWeekly = quotas.find((q) => q.modelKey === "claude_gpt_weekly");
expect(geminiWeekly).toMatchObject({
used: 250,
total: 1000,
remainingPercentage: 75,
resetAt: "2026-09-15T00:00:00Z",
});
expect(claudeWeekly).toMatchObject({
used: 500,
total: 1000,
remainingPercentage: 50,
resetAt: "2026-09-14T00:00:00Z",
});
});
it("weekly rows do NOT appear as otherModels", () => {
const quotas = parseQuotaData("antigravity", data);
const weeklyRows = quotas.filter((q) =>
q.modelKey === "gemini_weekly" || q.modelKey === "claude_gpt_weekly"
);
expect(weeklyRows).toHaveLength(2);
expect(weeklyRows[0].name).toMatch(/Weekly/);
expect(weeklyRows[1].name).toMatch(/Weekly/);
});
it("order: gemini family, claude family, weekly, then other", () => {
const quotas = parseQuotaData("antigravity", data);
const keys = quotas.map((q) => q.modelKey);
const geminiIdx = keys.indexOf("gemini");
const claudeIdx = keys.indexOf("claude");
const geminiWeeklyIdx = keys.indexOf("gemini_weekly");
const claudeWeeklyIdx = keys.indexOf("claude_gpt_weekly");
expect(geminiIdx).toBeLessThan(geminiWeeklyIdx);
expect(claudeIdx).toBeLessThan(claudeWeeklyIdx);
});
it("works with no weekly keys present (backward compat)", () => {
const noWeekly = {
quotas: {
"gemini-pro-agent": {
displayName: "Gemini 3.1 Pro (High)",
used: 200,
total: 1000,
remainingPercentage: 80,
},
},
};
const quotas = parseQuotaData("antigravity", noWeekly);
expect(quotas).toHaveLength(1);
expect(quotas[0].name).toBe("Gemini (Flash / Pro)");
});
});
+464
View File
@@ -0,0 +1,464 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock proxyAwareFetch before any imports that use it
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import {
parseWeeklyQuotaSummary,
fetchAntigravityWeeklyQuota,
_clearWeeklyCache,
} from "../../open-sse/services/usage/antigravity-weekly.js";
// — Fixtures ——————————————————————————————————————————————
const GEMINI_GROUP = {
displayName: "Gemini Models",
buckets: [
{
bucketId: "gemini-weekly-bucket",
displayName: "Weekly Limit",
remainingFraction: 0.75,
resetTime: "2026-09-15T00:00:00Z",
},
{
bucketId: "gemini-daily-bucket",
displayName: "Daily Limit",
remainingFraction: 0.9,
resetTime: "2026-09-09T00:00:00Z",
},
],
};
const CLAUDE_GPT_GROUP = {
displayName: "Claude and GPT models",
buckets: [
{
bucketId: "claude-gpt-weekly",
displayName: "Weekly Quota",
remainingFraction: 0.5,
resetTime: "2026-09-14T00:00:00Z",
},
],
};
const FULL_RESPONSE = { groups: [GEMINI_GROUP, CLAUDE_GPT_GROUP] };
const NESTED_RESPONSE = {
quotaSummary: {
groups: [GEMINI_GROUP, CLAUDE_GPT_GROUP],
},
};
// — parseWeeklyQuotaSummary ———————————————————————————————
describe("parseWeeklyQuotaSummary", () => {
it("extracts Gemini weekly quota from top-level groups", () => {
const result = parseWeeklyQuotaSummary(FULL_RESPONSE);
expect(result.gemini_weekly).toMatchObject({
used: 250,
total: 1000,
remainingPercentage: 75,
displayName: "Gemini (Weekly)",
unlimited: false,
});
expect(result.gemini_weekly.resetAt).toBe("2026-09-15T00:00:00.000Z");
});
it("extracts Claude & GPT weekly quota", () => {
const result = parseWeeklyQuotaSummary(FULL_RESPONSE);
expect(result.claude_gpt_weekly).toMatchObject({
used: 500,
total: 1000,
remainingPercentage: 50,
displayName: "Claude & GPT (Weekly)",
unlimited: false,
});
expect(result.claude_gpt_weekly.resetAt).toBe("2026-09-14T00:00:00.000Z");
});
it("handles alternate nested quotaSummary.groups shape", () => {
const result = parseWeeklyQuotaSummary(NESTED_RESPONSE);
expect(result.gemini_weekly).toBeDefined();
expect(result.claude_gpt_weekly).toBeDefined();
expect(result.gemini_weekly.remainingPercentage).toBe(75);
expect(result.claude_gpt_weekly.remainingPercentage).toBe(50);
});
it("skips non-weekly buckets", () => {
const data = {
groups: [{
displayName: "Gemini Models",
buckets: [
{
bucketId: "gemini-daily-bucket",
displayName: "Daily Limit",
remainingFraction: 0.9,
resetTime: "2026-09-09T00:00:00Z",
},
],
}],
};
const result = parseWeeklyQuotaSummary(data);
expect(result).toEqual({});
});
it("skips disabled weekly buckets", () => {
const data = {
groups: [{
displayName: "Gemini Models",
buckets: [{
bucketId: "gemini-weekly-bucket",
displayName: "Weekly Limit",
remainingFraction: 0.75,
resetTime: "2026-09-15T00:00:00Z",
disabled: true,
}],
}],
};
const result = parseWeeklyQuotaSummary(data);
expect(result).toEqual({});
});
it("returns empty object for null/undefined input", () => {
expect(parseWeeklyQuotaSummary(null)).toEqual({});
expect(parseWeeklyQuotaSummary(undefined)).toEqual({});
expect(parseWeeklyQuotaSummary("string")).toEqual({});
});
it("returns empty object for response with no groups", () => {
expect(parseWeeklyQuotaSummary({})).toEqual({});
expect(parseWeeklyQuotaSummary({ groups: "not-array" })).toEqual({});
expect(parseWeeklyQuotaSummary({ quotaSummary: {} })).toEqual({});
});
it("handles groups with no buckets gracefully", () => {
const data = {
groups: [{ displayName: "Gemini Models" }],
};
expect(parseWeeklyQuotaSummary(data)).toEqual({});
});
it("handles bucket with non-finite remainingFraction", () => {
const data = {
groups: [{
displayName: "Gemini Models",
buckets: [{
bucketId: "weekly-bucket",
displayName: "Weekly",
remainingFraction: "not-a-number",
}],
}],
};
expect(parseWeeklyQuotaSummary(data)).toEqual({});
});
it("ignores groups that don't match known families", () => {
const data = {
groups: [{
displayName: "Unknown AI Provider",
buckets: [{
bucketId: "weekly-bucket",
displayName: "Weekly",
remainingFraction: 0.5,
}],
}],
};
expect(parseWeeklyQuotaSummary(data)).toEqual({});
});
});
// — fetchAntigravityWeeklyQuota ———————————————————————————
describe("fetchAntigravityWeeklyQuota", () => {
beforeEach(() => {
proxyAwareFetch.mockReset();
_clearWeeklyCache();
});
it("fetches and returns parsed weekly quota on success", async () => {
proxyAwareFetch.mockResolvedValue({
ok: true,
json: async () => FULL_RESPONSE,
});
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result.gemini_weekly).toBeDefined();
expect(result.claude_gpt_weekly).toBeDefined();
});
it("returns {} on HTTP 401", async () => {
proxyAwareFetch.mockResolvedValue({ ok: false, status: 401 });
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("returns {} on HTTP 403", async () => {
proxyAwareFetch.mockResolvedValue({ ok: false, status: 403 });
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("returns {} on HTTP 404", async () => {
proxyAwareFetch.mockResolvedValue({ ok: false, status: 404 });
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("returns {} on HTTP 429", async () => {
proxyAwareFetch.mockResolvedValue({ ok: false, status: 429 });
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("returns {} on network error", async () => {
proxyAwareFetch.mockRejectedValue(new Error("network timeout"));
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("returns {} on malformed JSON response", async () => {
proxyAwareFetch.mockResolvedValue({
ok: true,
json: async () => { throw new SyntaxError("Unexpected token"); },
});
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(result).toEqual({});
});
it("deduplicates concurrent requests for the same account", async () => {
let resolveResponse;
proxyAwareFetch.mockReturnValue(new Promise(resolve => {
resolveResponse = resolve;
}));
const p1 = fetchAntigravityWeeklyQuota("token", "project-1");
const p2 = fetchAntigravityWeeklyQuota("token", "project-1");
resolveResponse({ ok: true, json: async () => FULL_RESPONSE });
const [r1, r2] = await Promise.all([p1, p2]);
expect(r1).toEqual(r2);
expect(proxyAwareFetch).toHaveBeenCalledTimes(1);
});
it("serves cached result within TTL", async () => {
proxyAwareFetch.mockResolvedValue({
ok: true,
json: async () => FULL_RESPONSE,
});
await fetchAntigravityWeeklyQuota("token", "project-1");
const result = await fetchAntigravityWeeklyQuota("token", "project-1");
expect(proxyAwareFetch).toHaveBeenCalledTimes(1);
expect(result.gemini_weekly).toBeDefined();
});
it("sends correct headers and body", async () => {
proxyAwareFetch.mockResolvedValue({
ok: true,
json: async () => ({ groups: [] }),
});
await fetchAntigravityWeeklyQuota("token", "project-1");
expect(proxyAwareFetch).toHaveBeenCalledWith(
"https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Authorization": "Bearer token",
"User-Agent": "antigravity/ide/2.11.0 darwin/arm64",
"Content-Type": "application/json",
"X-Client-Name": "antigravity",
}),
body: JSON.stringify({ project: "project-1" }),
}),
expect.any(Object),
);
});
});
// — Integration: weekly failure does not affect existing quotas —————
describe("weekly quota isolation from existing quota", () => {
beforeEach(() => {
proxyAwareFetch.mockReset();
_clearWeeklyCache();
});
it("existing getAntigravityUsage succeeds even when weekly RPC fails", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (url.includes(":loadCodeAssist")) {
return {
ok: true,
status: 200,
json: async () => ({ cloudaicompanionProject: "p1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }),
};
}
if (url.includes(":fetchAvailableModels")) {
return {
ok: true,
status: 200,
json: async () => ({
models: {
"gemini-3.8-flash-high": {
displayName: "Gemini 3.8 Flash (High)",
quotaInfo: { remainingFraction: 0.85, resetTime: "2026-09-15T00:00:00Z" },
},
},
}),
};
}
if (url.includes(":retrieveUserQuotaSummary")) {
throw new Error("weekly endpoint unavailable");
}
return { ok: false, status: 404 };
});
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
const result = await getAntigravityUsage("token", {});
expect(result.quotas["gemini-3.8-flash-high"]).toMatchObject({
used: 150,
total: 1000,
remainingPercentage: 85,
});
expect(result.quotas.gemini_weekly).toBeUndefined();
expect(result.quotas.claude_gpt_weekly).toBeUndefined();
expect(result.message).toBeUndefined();
});
it("free-tier accounts only show weekly quotas, not per-model short-window quotas", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (url.includes(":loadCodeAssist")) {
return {
ok: true,
status: 200,
json: async () => ({ cloudaicompanionProject: "p1", currentTier: { name: "Starter" }, paidTier: { id: "free-tier", name: "Antigravity Starter Quota" } }),
};
}
if (url.includes(":fetchAvailableModels")) {
return {
ok: true,
status: 200,
json: async () => ({
models: {
"gemini-3.8-flash-high": {
displayName: "Gemini 3.8 Flash (High)",
quotaInfo: { remainingFraction: 1, resetTime: "2026-09-15T00:00:00Z" },
},
"claude-sonnet-4-6": {
displayName: "Claude Sonnet 4.6",
// Missing remainingFraction — free tier exhausted
quotaInfo: { resetTime: "2026-09-13T12:00:00Z" },
},
},
}),
};
}
if (url.includes(":retrieveUserQuotaSummary")) {
return {
ok: true,
status: 200,
json: async () => ({
groups: [{
displayName: "Gemini Models",
buckets: [{
bucketId: "gemini-weekly",
displayName: "Weekly Limit Remaining",
remainingFraction: 1,
resetTime: "2026-09-15T00:00:00Z",
}],
}, {
displayName: "Claude and GPT models",
buckets: [{
bucketId: "3p-weekly",
displayName: "Weekly Limit Remaining",
remainingFraction: 0,
resetTime: "2026-09-13T12:00:00Z",
}],
}],
}),
};
}
return { ok: false, status: 404 };
});
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
const result = await getAntigravityUsage("token", {});
// Per-model quotas should be absent (free-tier accounts skip model parsing)
expect(result.quotas["gemini-3.8-flash-high"]).toBeUndefined();
expect(result.quotas["claude-sonnet-4-6"]).toBeUndefined();
// Only weekly quotas should appear
expect(result.quotas.gemini_weekly).toMatchObject({
used: 0,
total: 1000,
remainingPercentage: 100,
});
expect(result.quotas.claude_gpt_weekly).toMatchObject({
used: 1000,
total: 1000,
remainingPercentage: 0,
});
});
it("reconciles weekly quota to 0% when all paid-tier family models are exhausted", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (url.includes(":loadCodeAssist")) {
return {
ok: true,
status: 200,
json: async () => ({ cloudaicompanionProject: "p1", currentTier: { name: "Pro" }, paidTier: { id: "g1-pro-tier", name: "Google AI Pro" } }),
};
}
if (url.includes(":fetchAvailableModels")) {
return {
ok: true,
status: 200,
json: async () => ({
models: {
"gemini-3.8-flash-high": {
displayName: "Gemini 3.8 Flash (High)",
// Exhausted model: no remainingFraction, future resetTime
quotaInfo: { resetTime: "2026-09-13T12:00:00Z" },
},
},
}),
};
}
if (url.includes(":retrieveUserQuotaSummary")) {
return {
ok: true,
status: 200,
json: async () => ({
groups: [{
displayName: "Gemini Models",
buckets: [{
bucketId: "gemini-weekly",
displayName: "Weekly Limit Remaining",
remainingFraction: 1,
resetTime: "2026-09-15T00:00:00Z",
}],
}],
}),
};
}
return { ok: false, status: 404 };
});
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
const result = await getAntigravityUsage("token", {});
// Per-model quota should show exhausted
expect(result.quotas["gemini-3.8-flash-high"].remainingPercentage).toBe(0);
// Weekly quota should be reconciled to 0% with the family reset time
expect(result.quotas.gemini_weekly).toMatchObject({
used: 1000,
total: 1000,
remainingPercentage: 0,
resetAt: "2026-09-13T12:00:00.000Z",
});
});
});
@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import airforce from "../../open-sse/providers/registry/api-airforce.js";
import { PROVIDERS, PROVIDER_MODELS } from "../../open-sse/providers/index.js";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
describe("api-airforce free models", () => {
const ids = airforce.models.map((m) => m.id);
it("registers the three live free models", () => {
expect(ids).toContain("gpt-oss-120b");
expect(ids).toContain("gpt-oss-20b");
expect(ids).toContain("kimi-k2.7-code");
});
it("drops the dead catalog ids", () => {
expect(ids).not.toContain("anthropic/claude-3.7-sonnet");
expect(ids).not.toContain("moonshot/kimi-k2.6");
expect(ids).not.toContain("google/gemini-2.5-flash");
});
it("is passthrough so any live id resolves", () => {
expect(airforce.passthroughModels).toBe(true);
expect(PROVIDERS["api-airforce"].forceStream).toBe(true);
});
it("PROVIDER_MODELS['af'] exposes the new ids", () => {
expect(PROVIDER_MODELS.af.map((m) => m.id)).toEqual(expect.arrayContaining([
"gpt-oss-120b", "gpt-oss-20b", "kimi-k2.7-code",
]));
});
it("caps resolve for the free ids", () => {
expect(getCapabilitiesForModel("api-airforce", "kimi-k2.7-code").reasoning).toBe(true);
expect(getCapabilitiesForModel("api-airforce", "gpt-oss-120b").reasoning).toBe(true);
});
});
+51
View File
@@ -2,6 +2,24 @@ import { describe, expect, it } from "vitest";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
describe("getCapabilitiesForModel", () => {
it("reports DeepSeek V4.1-Flash ids as vision-capable without dropping their thinking/context", () => {
const v41 = { vision: true, reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 };
expect(getCapabilitiesForModel(undefined, "deepseek-v4.1-flash")).toMatchObject(v41);
expect(getCapabilitiesForModel("opencode-go", "deepseek-v4.1-flash")).toMatchObject(v41);
expect(getCapabilitiesForModel("openrouter", "deepseek/deepseek-v4.1-flash")).toMatchObject(v41);
// "deepseek-flash" is the GA id for V4.1-Flash on the DeepSeek API; the pattern it
// used to fall through to gives it 128K/64K, which the exact entry keeps.
expect(getCapabilitiesForModel("opencode-go", "deepseek-flash")).toMatchObject({
vision: true,
reasoning: true,
thinkingFormat: "deepseek",
contextWindow: 128000,
maxOutput: 64000,
});
// the superseded text-only Flash id stays text-only
expect(getCapabilitiesForModel("opencode-go", "deepseek-v4-flash").vision).toBe(false);
});
const claudeSonnet5Expected = {
contextWindow: 1000000,
maxOutput: 128000,
@@ -62,4 +80,37 @@ describe("getCapabilitiesForModel", () => {
expect(getCapabilitiesForModel("kiro", "gpt-5.6-luna-agentic")).toMatchObject(kiroGpt56Expected);
expect(getCapabilitiesForModel("kiro", "gpt-5.6-sol-thinking-agentic")).toMatchObject(kiroGpt56Expected);
});
it("reports Codex GPT 6.0 Astra as a vision and thinking capable model", () => {
expect(getCapabilitiesForModel("codex", "gpt-6-astra")).toMatchObject({
vision: true,
reasoning: true,
search: true,
thinkingFormat: "openai",
contextWindow: 272000,
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,
});
});
});
@@ -0,0 +1,222 @@
// #3795 — a proxy must not dispatch more cache_control blocks than Anthropic
// accepts, and must not lose turns that use single-object content (#3567 interplay).
import { describe, it, expect } from "vitest";
import {
anchorClaudeCache,
normalizeClaudePassthrough,
prepareClaudeRequest,
} from "../../open-sse/translator/formats/claude.js";
import { claudeToOpenAIRequest } from "../../open-sse/translator/request/claude-to-openai.js";
const CC = { type: "ephemeral" };
const text = (t, extra = {}) => ({ type: "text", text: t, ...extra });
const tool = (name, extra = {}) => ({ name, description: "d", input_schema: {}, ...extra });
// counts markers incl. single-object content — mirrors the upstream contract
function countMarkers(body) {
let n = 0;
if (Array.isArray(body.system)) for (const b of body.system) if (b?.cache_control) n++;
if (Array.isArray(body.tools)) for (const t of body.tools) if (t?.cache_control) n++;
if (Array.isArray(body.messages)) for (const m of body.messages) {
if (Array.isArray(m?.content)) {
for (const b of m.content) if (b?.cache_control) n++;
} else if (m?.content && typeof m.content === "object" && m.content.cache_control) n++;
}
return n;
}
describe("cache marker budget and single-block content", () => {
it("never emits more than four markers when the client already spent its budget", () => {
const out = anchorClaudeCache({
system: [text("s1"), text("s2", { cache_control: CC })],
tools: [tool("t1"), tool("t2", { cache_control: CC })],
messages: [
{ role: "user", content: text("u1", { cache_control: CC }) },
{ role: "assistant", content: text("a1", { cache_control: CC }) },
{ role: "user", content: [text("q")] },
],
});
expect(countMarkers(out)).toBeLessThanOrEqual(4); // base: 5
});
it("normalizes a single-object turn in passthrough and anchors it", () => {
const body = {
messages: [
{ role: "user", content: [text("u1")] },
{ role: "assistant", content: text("a1") }, // single object, no marker
{ role: "user", content: [text("q")] },
],
};
normalizeClaudePassthrough(body);
const assistant = body.messages.find(m => m.role === "assistant");
expect(assistant).toBeDefined();
expect(Array.isArray(assistant.content)).toBe(true); // base: still bare object
expect(assistant.content).toHaveLength(1);
const out = anchorClaudeCache(body);
expect(countMarkers(out)).toBe(1);
});
it("keeps a turn whose content is a single object and strips its marker", () => {
const out = prepareClaudeRequest({
model: "claude-sonnet-5", max_tokens: 100,
system: [text("s1")],
messages: [
{ role: "user", content: text("u1", { cache_control: CC }) },
{ role: "assistant", content: [text("a1")] },
{ role: "user", content: [text("q")] },
],
}, "claude");
const kept = out.messages.filter(m => JSON.stringify(m.content).includes("u1"));
expect(kept.length).toBe(1); // base: 0 (dropped)
expect(kept[0].content).toHaveLength(1); // normalized to array
expect(kept[0].content[0].cache_control).toBeUndefined();
});
it("drops no conversation turn when content is a single text object", () => {
const out = prepareClaudeRequest({
model: "claude-sonnet-5", max_tokens: 100,
messages: [
{ role: "user", content: text("u1") },
{ role: "assistant", content: text("a1") },
{ role: "user", content: [text("q")] },
],
}, "claude");
expect(out.messages.length).toBe(3); // base: 1
expect(Array.isArray(out.messages[0].content)).toBe(true);
});
it("re-anchors the last assistant turn even when it uses single-object content", () => {
const out = prepareClaudeRequest({
model: "claude-sonnet-5", max_tokens: 100,
messages: [
{ role: "user", content: [text("u1")] },
{ role: "assistant", content: text("a1") },
{ role: "user", content: [text("q")] },
],
}, "claude");
expect(countMarkers(out)).toBe(1); // base: 0
});
it("keeps a marked single-object turn when the marker budget is spent", () => {
const body = {
system: [text("s1", { cache_control: CC })],
tools: [tool("t1", { cache_control: CC })],
messages: [
{ role: "user", content: [text("c1"), text("c2")] },
{ role: "assistant", content: [text("a1", { cache_control: CC })] },
{ role: "user", content: text("u1", { cache_control: CC }) },
{ role: "user", content: [text("q")] },
],
};
normalizeClaudePassthrough(body);
const out = anchorClaudeCache(body);
const kept = out.messages.filter(m => JSON.stringify(m.content).includes("u1"));
expect(kept.length).toBe(1);
expect(Array.isArray(kept[0].content)).toBe(true); // base: bare object survives
expect(kept[0].content).toHaveLength(1);
expect(kept[0].content[0].cache_control).toBeUndefined();
const ctx = out.messages.find(m => JSON.stringify(m.content).includes("c1"));
expect(ctx.content).toEqual([text("c1"), text("c2")]);
expect(countMarkers(out)).toBeLessThanOrEqual(4); // fixed: 3
});
it("keeps single-object turns on the claude-to-openai leg", () => {
const out = claudeToOpenAIRequest("m", {
messages: [
{ role: "user", content: text("u1") },
{ role: "assistant", content: { type: "image", source: { type: "base64", media_type: "image/png", data: "iVBORw0KGgo=" } } },
],
}, false);
expect(out.messages.some(m => JSON.stringify(m.content).includes("u1"))).toBe(true); // base: dropped
const img = out.messages.find(m => m.role === "assistant");
expect(JSON.stringify(img.content)).toContain("image_url"); // base: dropped
});
it("keeps a bare-object user turn folded with a mid-conversation system message", () => {
const body = {
messages: [
{ role: "user", content: text("u1") },
{ role: "system", content: [text("reminder")] },
{ role: "user", content: [text("q")] },
],
};
normalizeClaudePassthrough(body);
const first = body.messages[0];
expect(Array.isArray(first.content)).toBe(true);
expect(JSON.stringify(first.content).includes("u1")).toBe(true); // pre-hoist: fold zeroes bare-object content
expect(JSON.stringify(first.content).includes("reminder")).toBe(true);
});
it("prunes a client body that already carries five markers down to four", () => {
const out = anchorClaudeCache({
system: [text("s1"), text("s2", { cache_control: CC })],
tools: [tool("t1", { cache_control: CC }), tool("t2", { cache_control: CC })],
messages: [
{ role: "user", content: [text("u1", { cache_control: CC })] },
{ role: "assistant", content: [text("a1", { cache_control: CC })] },
{ role: "user", content: [text("q")] },
],
});
expect(countMarkers(out)).toBe(4); // pre-fix: 5 forwarded unchanged
expect(out.system[0].cache_control).toBeUndefined(); // earliest marker pruned
});
// A spent budget must not cost the head anchors their 1h TTL: system/tools are
// the whole point of re-anchoring, and a 5m fallback silently halves the cache
// lifetime on exactly the requests that already cached aggressively.
it("keeps the 1h head anchors when the client spent the whole budget", () => {
const out = anchorClaudeCache({
system: [text("s1"), text("s2", { cache_control: CC })],
tools: [tool("t1", { cache_control: CC }), tool("t2")],
messages: [
{ role: "user", content: [text("u1", { cache_control: CC })] },
{ role: "assistant", content: [text("a1", { cache_control: CC })] },
{ role: "user", content: [text("q")] },
],
});
expect(countMarkers(out)).toBeLessThanOrEqual(4);
expect(out.system.at(-1).cache_control?.ttl).toBe("1h"); // pre-fix: fell back to 5m
expect(out.tools.at(-1).cache_control?.ttl).toBe("1h"); // pre-fix: fell back to 5m
});
it("keeps the 1h head anchors on an over-budget body", () => {
const out = anchorClaudeCache({
system: [text("s1", { cache_control: CC })],
tools: [tool("t1", { cache_control: CC }), tool("t2")],
messages: [
{ role: "user", content: [text("u1", { cache_control: CC })] },
{ role: "assistant", content: [text("a1", { cache_control: CC })] },
{ role: "user", content: [text("u2", { cache_control: CC })] },
{ role: "assistant", content: [text("a2", { cache_control: CC })] },
{ role: "user", content: [text("q")] },
],
});
expect(countMarkers(out)).toBe(4);
expect(out.system.at(-1).cache_control?.ttl).toBe("1h");
expect(out.tools.at(-1).cache_control?.ttl).toBe("1h");
});
it("strips a marker from a deferred tool even when the budget is spent", () => {
const out = anchorClaudeCache({
system: [text("s1", { cache_control: CC })],
tools: [tool("t1", { cache_control: CC, defer_loading: true })],
messages: [
{ role: "user", content: [text("u1", { cache_control: CC })] },
{ role: "assistant", content: [text("a1", { cache_control: CC })] },
{ role: "user", content: [text("q")] },
],
});
expect(countMarkers(out)).toBeLessThanOrEqual(4);
const deferred = out.tools.find(t => t.defer_loading);
expect(deferred?.cache_control).toBeUndefined(); // pre-fix: invalid marker forwarded
});
it("keeps a bare-object system reminder on the claude-to-openai leg", () => {
const out = claudeToOpenAIRequest("m", {
messages: [
{ role: "user", content: "hi" },
{ role: "system", content: text("be brief") },
],
}, false);
expect(JSON.stringify(out.messages)).toContain("be brief"); // pre-fix: turn dropped
});
});
@@ -187,6 +187,46 @@ describe("DefaultExecutor.buildHeaders() — anthropic-compatible stripping", ()
headers["Anthropic-Version"] || headers["anthropic-version"];
expect(hasVersion).toBeDefined();
});
// A node fronting Anthropic (rotating multi-account proxy, corporate gateway)
// needs the same beta flags the `claude` provider sends. Without
// context-management-2025-06-27 upstream answers HTTP 400
// "context_management: Extra inputs are not permitted" and the combo falls
// through to the next model without anyone noticing.
it("sends context-management beta for a Claude model on a custom host", () => {
const executor = new DefaultExecutor("anthropic-compatible-custom");
const headers = executor.buildHeaders(
{
apiKey: "key",
providerSpecificData: { baseUrl: "https://myproxy.example.com/v1" },
},
true,
undefined,
"claude-opus-5"
);
const betaFlags = (headers["Anthropic-Beta"] || headers["anthropic-beta"] || "")
.split(",").map(s => s.trim());
expect(betaFlags).toContain("context-management-2025-06-27");
// The first-party identity flag is still stripped for a non-Anthropic host.
expect(betaFlags).not.toContain("claude-code-20250219");
});
it("gates the beta flags on the model id, not the provider prefix", () => {
const executor = new DefaultExecutor("anthropic-compatible-custom");
const headers = executor.buildHeaders(
{
apiKey: "key",
providerSpecificData: { baseUrl: "https://myproxy.example.com/v1" },
},
true,
undefined,
"kimi-k3"
);
const betaVal = headers["Anthropic-Beta"] || headers["anthropic-beta"] || "";
expect(betaVal).not.toContain("context-management-2025-06-27");
});
});
// ─── proxyFetch anthropicFetch routing ────────────────────────────────────────
+40
View File
@@ -0,0 +1,40 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getClineAccessToken,
getClineAuthorizationHeader,
} from "../../open-sse/shared/clineAuth.js";
test("getClineAccessToken keeps an existing workos: prefix", () => {
const token = "workos:eyJhbGciOiJSUzI1NiJ9.eyJwYXAiJ9";
assert.equal(getClineAccessToken(token), token);
assert.equal(getClineAccessToken(` ${token} `), token);
});
test("getClineAccessToken prefixes a bare WorkOS JWT with workos:", () => {
const jwt = "eyJhbGciOiJSUzI1NiJ9.eyJwYXAiJ9";
assert.equal(getClineAccessToken(jwt), `workos:${jwt}`);
});
test("getClineAccessToken does NOT prefix ClinePass API keys", () => {
// ClinePass API keys are opaque strings (e.g. clp_…). Sending them as
// `workos:clp_…` makes api.cline.bot respond 401.
assert.equal(getClineAccessToken("clp_1234567890abcdef"), "clp_1234567890abcdef");
assert.equal(getClineAccessToken("sk-9r-abcdef"), "sk-9r-abcdef");
assert.equal(getClineAccessToken(""), "");
assert.equal(getClineAccessToken(" "), "");
assert.equal(getClineAccessToken(undefined), "");
assert.equal(getClineAccessToken(null), "");
});
test("getClineAuthorizationHeader builds a Bearer header without double prefixing", () => {
assert.equal(getClineAuthorizationHeader("clp_abc"), "Bearer clp_abc");
assert.equal(
getClineAuthorizationHeader("eyJpeg.eyJbG"),
"Bearer workos:eyJpeg.eyJbG"
);
assert.equal(
getClineAuthorizationHeader("workos:eyJpeg.eyJbG"),
"Bearer workos:eyJpeg.eyJbG"
);
});
@@ -0,0 +1,297 @@
// Cline free models (z-ai/glm-5.3-flash, deepseek-v4-flash) wrap non-stream
// chat completions in {"success":true,"data":{...choices...}} on
// https://api.cline.bot/api/v1/chat/completions. Both the UI model-test ping
// (src/app/api/models/test/ping.js) and the proxy non-stream path
// (open-sse/handlers/chatCore/nonStreamingHandler.js) read `choices` at the
// top level, so enveloped choices are invisible ("Provider returned no
// completion choices for this model"). These tests pin the unwrap behavior.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock the heavy Next.js-dependent imports BEFORE importing ping.js
// (same pattern as tests/unit/ping-reasoning-models-3010.test.js).
vi.mock("@/lib/localDb", () => ({ getApiKeys: vi.fn(async () => [{ key: "test-key", isActive: true }]) }));
vi.mock("@/shared/constants/config", () => ({ UPDATER_CONFIG: { appPort: 20127 } }));
vi.mock("@/shared/utils/machineId", () => ({ getConsistentMachineId: vi.fn(async () => "cli-token") }));
// requestDetail.js imports from @/lib/usageDb.js too, so one mock covers both
// the handler and its usage/detail helpers.
vi.mock("@/lib/usageDb.js", () => ({
appendRequestLog: vi.fn(async () => {}),
saveRequestDetail: vi.fn(async () => {}),
saveRequestUsage: vi.fn(async () => {}),
}));
const { pingModelByKind } = await import("../../src/app/api/models/test/ping.js");
const { handleNonStreamingResponse } = await import("../../open-sse/handlers/chatCore/nonStreamingHandler.js");
// The proxy adds a 2000-token headroom buffer to usage before returning it
// to the client (addBufferToUsage), so response-body usage is input + 2000.
// The usage recorded via saveRequestUsage is the unbuffered extraction —
// asserting on it proves the unwrap ran before usage extraction.
const { saveRequestUsage } = await import("@/lib/usageDb.js");
describe("cline free-models {success,data} envelope", () => {
let fetchMock;
beforeEach(() => {
fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
function jsonResponse(obj) {
return {
ok: true,
status: 200,
text: async () => JSON.stringify(obj),
json: async () => obj,
};
}
it("ping: enveloped success unwraps to ok:true (regression for reported error)", async () => {
fetchMock.mockResolvedValue(
jsonResponse({ success: true, data: { choices: [{ message: { content: "OK" } }] } })
);
const result = await pingModelByKind("cl/z-ai/glm-5.3-flash", "llm", "http://127.0.0.1:20127");
expect(result.ok).toBe(true);
});
it("ping: enveloped reasoning-only response still soft-passes with note", async () => {
fetchMock.mockResolvedValue(
jsonResponse({
success: true,
data: {
choices: [
{
finish_reason: "length",
message: { content: "", reasoning: "The user said hi" },
},
],
},
})
);
const result = await pingModelByKind("cl/z-ai/glm-5.3-flash", "llm", "http://127.0.0.1:20127");
expect(result.ok).toBe(true);
expect(result.note).toMatch(/reasoning-only/);
});
it("ping: error envelope passes through without unwrap", async () => {
const body = { error: "empty response content", success: false };
fetchMock.mockResolvedValue({
ok: false,
status: 500,
text: async () => JSON.stringify(body),
json: async () => body,
});
const result = await pingModelByKind("cl/z-ai/glm-5.3-flash", "llm", "http://127.0.0.1:20127");
expect(result.ok).toBe(false);
expect(result.error).toMatch(/empty response content/);
});
it("ping: bare (un-enveloped) body still passes", async () => {
fetchMock.mockResolvedValue(jsonResponse({ choices: [{ message: { content: "Hello!" } }] }));
const result = await pingModelByKind("openai/gpt-4o", "llm", "http://127.0.0.1:20127");
expect(result.ok).toBe(true);
});
it("ping: does not unwrap for a provider that did not opt in", async () => {
fetchMock.mockResolvedValue(
jsonResponse({ success: true, data: { choices: [{ message: { content: "OK" } }] } })
);
const result = await pingModelByKind("openai/gpt-4o", "llm", "http://127.0.0.1:20127");
expect(result.ok).toBe(false);
expect(result.error).toMatch(/no completion choices/i);
});
});
describe("cline free-models envelope in nonStreamingHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
});
function stubLogger() {
return { logProviderResponse() {}, logConvertedResponse() {} };
}
function callHandler(providerResponse, provider = "cline") {
return handleNonStreamingResponse({
providerResponse,
provider,
model: "z-ai/glm-5.3-flash",
sourceFormat: "openai",
targetFormat: "openai",
body: { stream: false },
stream: false,
translatedBody: null,
finalBody: null,
requestStartTime: Date.now(),
connectionId: "c1",
apiKey: "k",
clientRawRequest: null,
onRequestSuccess: () => {},
reqLogger: stubLogger(),
toolNameMap: null,
customToolNames: null,
trackDone: () => {},
appendLog: () => {},
pxpipe: null,
reqTag: "t",
log: null,
});
}
it("unwraps the {success,data} envelope before usage extraction and translation", async () => {
const providerResponse = new Response(
JSON.stringify({
success: true,
data: {
choices: [{ message: { content: "Hi" } }],
usage: { prompt_tokens: 5, completion_tokens: 2 },
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
const result = await callHandler(providerResponse);
expect(result.success).toBe(true);
const body = await result.response.json();
expect(body.choices).toBeDefined();
expect(body.choices[0].message.content).toBe("Hi");
expect(body.success).toBeUndefined();
expect(saveRequestUsage).toHaveBeenCalledTimes(1);
expect(saveRequestUsage.mock.calls[0][0].tokens).toMatchObject({
prompt_tokens: 5,
completion_tokens: 2,
});
expect(body.usage.prompt_tokens).toBe(2005);
});
it("passes a bare (non-enveloped) body through unchanged", async () => {
const providerResponse = new Response(
JSON.stringify({
choices: [{ message: { content: "Hi" } }],
usage: { prompt_tokens: 3, completion_tokens: 1 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
const result = await callHandler(providerResponse);
expect(result.success).toBe(true);
const body = await result.response.json();
expect(body.choices[0].message.content).toBe("Hi");
expect(saveRequestUsage).toHaveBeenCalledTimes(1);
expect(saveRequestUsage.mock.calls[0][0].tokens).toMatchObject({
prompt_tokens: 3,
completion_tokens: 1,
});
expect(body.usage.prompt_tokens).toBe(2003);
});
// The unwrap is opt-in via transport.quirks.clineEnvelope so it can never
// rewrite another provider's body — including one that happens to return
// {"success":true,"data":...} for its own reasons.
it("leaves an enveloped body untouched for a provider that did not opt in", async () => {
const providerResponse = new Response(
JSON.stringify({
success: true,
data: { choices: [{ message: { content: "Hi" } }] },
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
const result = await callHandler(providerResponse, "openai");
const body = await result.response.json();
expect(body.success).toBe(true);
expect(body.data.choices[0].message.content).toBe("Hi");
expect(body.choices).toBeUndefined();
});
});
describe("cline /api/v1/models aggregation (resolveClineModels vs resolveClinepassModels)", () => {
const API_MODELS_URL = "https://api.cline.bot/api/v1/models";
const API_RESPONSE = [
{ id: "cline-pass/deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "cline-pass/glm-5.2", name: "GLM-5.2" },
{ id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash" },
{ id: "z-ai/deepseek-v4-flash", name: "DeepSeek V4 Flash (Free)" },
];
let fetchMock;
beforeEach(() => {
fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("resolveClineModels returns all models (including free-tier)", async () => {
const { resolveClineModels } = await import("../../open-sse/services/clinepassModels.js");
fetchMock.mockResolvedValue({
ok: true,
json: async () => API_RESPONSE,
});
const result = await resolveClineModels({ accessToken: "test-token" });
expect(result).not.toBeNull();
expect(result.models).toHaveLength(4);
const ids = result.models.map((m) => m.id);
expect(ids).toContain("cline-pass/deepseek-v4-flash");
expect(ids).toContain("z-ai/glm-5.3-flash");
expect(ids).toContain("z-ai/deepseek-v4-flash");
});
it("resolveClinepassModels returns only cline-pass/ models", async () => {
const { resolveClinepassModels } = await import("../../open-sse/services/clinepassModels.js");
fetchMock.mockResolvedValue({
ok: true,
json: async () => API_RESPONSE,
});
const result = await resolveClinepassModels({ accessToken: "test-token" });
expect(result).not.toBeNull();
expect(result.models).toHaveLength(2);
const ids = result.models.map((m) => m.id);
expect(ids).toContain("cline-pass/deepseek-v4-flash");
expect(ids).toContain("cline-pass/glm-5.2");
expect(ids).not.toContain("z-ai/glm-5.3-flash");
});
it("resolveClineModels unwraps {success,data} envelope", async () => {
const { resolveClineModels } = await import("../../open-sse/services/clinepassModels.js");
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({ success: true, data: API_RESPONSE }),
});
const result = await resolveClineModels({ accessToken: "test-token" });
expect(result).not.toBeNull();
expect(result.models).toHaveLength(4);
});
it("resolveClineModels returns null when no token", async () => {
const { resolveClineModels } = await import("../../open-sse/services/clinepassModels.js");
const result = await resolveClineModels({});
expect(result).toBeNull();
});
it("resolveClineModels returns null on fetch error", async () => {
const { resolveClineModels } = await import("../../open-sse/services/clinepassModels.js");
fetchMock.mockRejectedValue(new Error("network error"));
const result = await resolveClineModels({ accessToken: "test-token" });
expect(result).toBeNull();
});
it("resolveClineModels returns {id,name} shape", async () => {
const { resolveClineModels } = await import("../../open-sse/services/clinepassModels.js");
fetchMock.mockResolvedValue({
ok: true,
json: async () => API_RESPONSE,
});
const result = await resolveClineModels({ accessToken: "test-token" });
expect(result.models[0]).toHaveProperty("id");
expect(result.models[0]).toHaveProperty("name");
expect(typeof result.models[0].id).toBe("string");
expect(typeof result.models[0].name).toBe("string");
});
});
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import {
getDefaultModel,
getModelQuotaFamily,
getModelUpstreamId,
getProviderModels,
} from "../../open-sse/config/providerModels.js";
import { getModelInfoCore } from "../../open-sse/services/model.js";
// Codex CLI's auto-review sends the bare model id "codex-auto-review". Before #1398 it fell
// through prefix inference to the "openai" default and failed with
// "No active credentials for provider: openai".
describe("codex auto-review routing (#1398)", () => {
it("routes the bare Codex auto-review model to the OAuth Codex provider", async () => {
await expect(getModelInfoCore("codex-auto-review", {})).resolves.toEqual({
provider: "codex",
model: "codex-auto-review",
});
});
it("exposes Codex auto-review as a review-quota Codex model", () => {
const autoReview = getProviderModels("cx").find(
(model) => model.id === "codex-auto-review",
);
expect(autoReview).toBeTruthy();
expect(autoReview.name).toBe("Codex Auto Review");
expect(getModelQuotaFamily("cx", "codex-auto-review")).toBe("review");
});
// getModelUpstreamId strips CODEX_REVIEW_SUFFIX from unregistered "cx" ids, which would send
// "codex-auto" upstream. This model is not a derived review variant, so it must go out verbatim.
it("forwards the id upstream without stripping the -review suffix", () => {
expect(getModelUpstreamId("cx", "codex-auto-review")).toBe(
"codex-auto-review",
);
});
// Registering it must not push it to the front of the cx list — getDefaultModel takes models[0].
it("does not become the default Codex model", () => {
expect(getDefaultModel("cx")).not.toBe("codex-auto-review");
});
});
+76
View File
@@ -0,0 +1,76 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getModelsByProviderId, getModelType, isValidModel } from "../../open-sse/config/providerModels.js";
import { getModelInfoCore } from "../../open-sse/services/model.js";
import { handleImageGenerationCore } from "../../open-sse/handlers/imageGenerationCore.js";
const models = ["gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra"];
afterEach(() => vi.unstubAllGlobals());
describe("Codex GPT-5.6 image models", () => {
it.each(models)("exposes %s-image as an image model while retaining its chat entry", (model) => {
const catalog = getModelsByProviderId("codex");
expect(catalog.filter((entry) => entry.id === `${model}-image`)).toHaveLength(1);
expect(catalog.find((entry) => entry.id === `${model}-image`)).toMatchObject({
kind: "image",
capabilities: ["text2img", "edit"],
params: ["size", "quality", "background", "image_detail", "output_format"],
});
expect(isValidModel("cx", `${model}-image`)).toBe(true);
expect(getModelType("cx", `${model}-image`)).toBe("image");
expect(catalog.find((entry) => entry.id === model)).toBeDefined();
expect(getModelType("cx", model)).not.toBe("image");
});
it.each(models)("routes %s-image edits and streams image events", async (model) => {
const events = [
["response.image_generation_call.partial_image", { partial_image_b64: "cGFydGlhbA==", partial_image_index: 0 }],
["response.output_item.done", { item: { type: "image_generation_call", result: "ZmluYWw=" } }],
];
const fetchMock = vi.fn().mockResolvedValue(new Response(
events.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`).join(""),
{ headers: { "Content-Type": "text/event-stream" } },
));
vi.stubGlobal("fetch", fetchMock);
const onRequestSuccess = vi.fn();
const modelInfo = await getModelInfoCore(`cx/${model}-image`);
expect(modelInfo).toEqual({ provider: "codex", model: `${model}-image` });
expect(await getModelInfoCore(`codex/${model}-image`)).toEqual(modelInfo);
const result = await handleImageGenerationCore({
modelInfo,
body: {
prompt: "Make the square blue",
image: "data:image/png;base64,cmVmZXJlbmNl",
image_detail: "low",
size: "1024x1024",
quality: "high",
background: "transparent",
output_format: "WEBP",
},
credentials: { accessToken: "test-token" },
streamToClient: true,
onRequestSuccess,
});
expect(result.success).toBe(true);
expect(result.response.status).toBe(200);
expect(result.response.headers.get("content-type")).toBe("text/event-stream");
const [url, options] = fetchMock.mock.calls[0];
expect(url).toBe("https://chatgpt.com/backend-api/codex/responses");
const upstreamBody = JSON.parse(options.body);
expect(upstreamBody.model).toBe(model);
expect(upstreamBody.tools).toEqual([{
type: "image_generation", output_format: "webp", size: "1024x1024",
quality: "high", background: "transparent",
}]);
expect(upstreamBody.input[0].content).toContainEqual({
type: "input_image", image_url: "data:image/png;base64,cmVmZXJlbmNl", detail: "low",
});
const stream = await result.response.text();
expect(stream).toContain('event: partial_image\ndata: {"b64_json":"cGFydGlhbA==","index":0}');
expect(stream).toContain("event: done\n");
expect(stream).toContain('"data":[{"b64_json":"ZmluYWw="}]');
expect(onRequestSuccess).toHaveBeenCalledTimes(1);
});
});
+11
View File
@@ -91,6 +91,17 @@ describe("Codex reset credits", () => {
});
});
it("surfaces structured upstream errors as readable messages", async () => {
mocks.proxyAwareFetch.mockResolvedValue({
ok: false,
status: 403,
json: async () => ({ error: { message: "Reset credits are unavailable for this account" } }),
});
const { getCodexRateLimitResetCredits } = await import("../../open-sse/services/usage/codex.js");
await expect(getCodexRateLimitResetCredits("token")).rejects.toThrow("Reset credits are unavailable for this account");
});
it("GET refreshes OAuth credentials before returning reset credit details", async () => {
const connection = {
id: "conn_1",
@@ -118,6 +118,69 @@ describe("CodexExecutor tool normalization", () => {
]);
});
it("strips only Unicode-property patterns rejected by Codex", () => {
const unicodePattern = "^(?!__.*__$)[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]{1,200}$";
const validPattern = "^[a-z][a-z0-9_-]{0,31}$";
const sourceParameters = {
type: "object",
properties: {
artifact: {
type: "object",
properties: {
name: { type: "string", pattern: unicodePattern },
slug: { type: "string", pattern: validPattern },
},
},
// A property named "pattern" is data, not the schema keyword.
pattern: { type: "string", pattern: validPattern },
},
allOf: [{ properties: { title: { type: "string", pattern: unicodePattern } } }],
};
const tools = normalizeTools([{
type: "function",
name: "Artifact",
parameters: sourceParameters,
}]);
expect(tools[0].parameters.properties.artifact.properties.name.pattern).toBeUndefined();
expect(tools[0].parameters.properties.artifact.properties.slug.pattern).toBe(validPattern);
expect(tools[0].parameters.properties.pattern.pattern).toBe(validPattern);
expect(tools[0].parameters.allOf[0].properties.title.pattern).toBeUndefined();
// Copy-on-write: the caller's schema remains available for another provider.
expect(sourceParameters.properties.artifact.properties.name.pattern).toBe(unicodePattern);
});
it("keeps escaped literal property text and schema identity when no strip is needed", () => {
const parameters = {
type: "object",
properties: {
literal: { type: "string", pattern: "^\\\\p{Cc}$" },
simple: { type: "string", pattern: "^[A-Z]+$" },
},
};
const tools = normalizeTools([{ type: "function", name: "probe", parameters }]);
expect(tools[0].parameters).toBe(parameters);
expect(tools[0].parameters.properties.literal.pattern).toBe("^\\\\p{Cc}$");
});
it("sanitizes nested namespace function schemas", () => {
const tools = normalizeTools([{
type: "namespace",
name: "agent",
tools: [{
type: "function",
name: "Artifact",
parameters: {
type: "object",
properties: { name: { type: "string", pattern: "^\\p{Cc}+$" } },
},
}],
}]);
expect(tools[0].tools[0].parameters.properties.name.pattern).toBeUndefined();
});
it("preserves custom freeform tools with format payloads", () => {
const tools = normalizeTools([
{
+40
View File
@@ -132,6 +132,46 @@ describe("inspectAndWrapCommandCodeResponse", () => {
expect(text).toContain("Hello from Laguna");
expect(text).toContain("data: [DONE]");
});
it("retries when initial stream yields an error and succeeds on second attempt", async () => {
let callCount = 0;
const executor = new CommandCodeExecutor();
// Override execute on instance to test retry behavior
executor.execute = async (opts) => {
const maxRetries = 2;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
callCount++;
let rawResponse;
if (callCount === 1) {
rawResponse = new Response(createNdjsonStream([
JSON.stringify({
type: "error",
error: { type: "server_error", message: "Network connection lost." }
}) + "\n"
]), { status: 200, headers: { "Content-Type": "text/event-stream" } });
} else {
rawResponse = new Response(createNdjsonStream([
JSON.stringify({ type: "start" }) + "\n",
JSON.stringify({ type: "text-delta", text: "Recovered from lost connection" }) + "\n",
JSON.stringify({ type: "finish" }) + "\n"
]), { status: 200, headers: { "Content-Type": "text/event-stream" } });
}
const wrappedResponse = await inspectAndWrapCommandCodeResponse(rawResponse, opts.model);
if (!wrappedResponse.ok && attempt < maxRetries) {
continue;
}
return { response: wrappedResponse };
}
};
const res = await executor.execute({ model: "deepseek/deepseek-v4.1-flash" });
expect(res.response.ok).toBe(true);
expect(callCount).toBe(2);
const text = await res.response.text();
expect(text).toContain("Recovered from lost connection");
});
});
describe("CommandCode in Combo Fallback", () => {
+135
View 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 });
});
});
+61
View File
@@ -0,0 +1,61 @@
/**
* SSRF guard on POST /api/cli-tools/cowork-mcp-tools (#3782).
*
* Remote callers must not be able to force server-side fetches to
* internal URLs; local-host use (self-hosted MCP servers) keeps working.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("next/server", () => ({
NextResponse: {
json: (body, init) =>
new Response(JSON.stringify(body), {
status: init?.status ?? 200,
headers: { "content-type": "application/json" },
}),
},
}));
const { POST } = await import(
"../../src/app/api/cli-tools/cowork-mcp-tools/route.js"
);
function remoteRequest(url) {
return new Request("http://gateway.example.com/api/cli-tools/cowork-mcp-tools", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url }),
});
}
describe("cowork-mcp-tools SSRF guard", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("rejects loopback URLs from remote callers without fetching", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch");
const res = await POST(remoteRequest("http://127.0.0.1:18731/internal-admin"));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ error: "URL not allowed" });
expect(fetchSpy).not.toHaveBeenCalled();
});
it("rejects private-network URLs from remote callers", async () => {
for (const url of ["http://10.0.0.5/mcp", "http://192.168.1.1/mcp", "http://localhost:3000/mcp"]) {
const res = await POST(remoteRequest(url));
expect(res.status, `should reject ${url}`).toBe(400);
}
});
it("still requires a url", async () => {
const res = await POST(
new Request("http://gateway.example.com/api/cli-tools/cowork-mcp-tools", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
})
);
expect(res.status).toBe(400);
});
});
+98
View File
@@ -101,6 +101,104 @@ describe("DB SQLite layer — public API parity", () => {
expect(back.providerSpecificData).toEqual({ foo: "bar" });
});
it("providerConnections: successful validation clears stale routing locks", async () => {
const c = await sqliteDb.createProviderConnection({
provider: "health-reset-update",
authType: "oauth",
email: "update@example.com",
accessToken: "old-token",
});
await sqliteDb.updateProviderConnection(c.id, {
testStatus: "unavailable",
lastError: "Access denied",
lastErrorAt: "2026-09-05T00:00:00.000Z",
errorCode: 403,
backoffLevel: 3,
rateLimitedUntil: "2099-01-01T00:00:00.000Z",
modelLock_modelA: "2099-01-01T00:00:00.000Z",
modelLock_modelB: "2099-01-01T00:00:00.000Z",
});
await sqliteDb.updateProviderConnection(c.id, { testStatus: "active" });
const back = await sqliteDb.getProviderConnectionById(c.id);
expect(back).toMatchObject({
testStatus: "active",
lastError: null,
lastErrorAt: null,
errorCode: null,
backoffLevel: 0,
rateLimitedUntil: null,
modelLock_modelA: null,
modelLock_modelB: null,
});
});
it("providerConnections: re-saving valid OAuth credentials clears stale routing locks", async () => {
const existing = await sqliteDb.createProviderConnection({
provider: "health-reset-resave",
authType: "oauth",
email: "resave@example.com",
accessToken: "old-token",
});
await sqliteDb.updateProviderConnection(existing.id, {
testStatus: "unavailable",
lastError: "Access denied",
errorCode: 403,
backoffLevel: 2,
modelLock_modelA: "2099-01-01T00:00:00.000Z",
});
const resaved = await sqliteDb.createProviderConnection({
provider: "health-reset-resave",
authType: "oauth",
email: "resave@example.com",
accessToken: "new-token",
testStatus: "active",
});
expect(resaved.id).toBe(existing.id);
const back = await sqliteDb.getProviderConnectionById(existing.id);
expect(back).toMatchObject({
accessToken: "new-token",
testStatus: "active",
lastError: null,
errorCode: null,
backoffLevel: 0,
modelLock_modelA: null,
});
});
it("providerConnections: active soft warnings survive the health reset", async () => {
const c = await sqliteDb.createProviderConnection({
provider: "health-reset-warning",
authType: "oauth",
email: "warning@example.com",
});
await sqliteDb.updateProviderConnection(c.id, {
testStatus: "unavailable",
lastError: "Old failure",
modelLock_modelA: "2099-01-01T00:00:00.000Z",
});
const warningAt = "2026-09-06T00:00:00.000Z";
await sqliteDb.updateProviderConnection(c.id, {
testStatus: "active",
lastError: "Connected, but credits are exhausted",
lastErrorAt: warningAt,
});
const back = await sqliteDb.getProviderConnectionById(c.id);
expect(back).toMatchObject({
testStatus: "active",
lastError: "Connected, but credits are exhausted",
lastErrorAt: warningAt,
errorCode: null,
backoffLevel: 0,
modelLock_modelA: null,
});
});
it("providerConnections: GitHub OAuth uses account identity as fallback name", async () => {
const c = await sqliteDb.createProviderConnection({
provider: "github",
+145
View File
@@ -0,0 +1,145 @@
/**
* Regression test: prepareClaudeRequest() must strip client-defined `custom`
* tools when forwarding to a provider whose Anthropic-compatible endpoint
* does not accept them (DeepSeek — accepts only web_search_*).
*
* Background:
* When Claude Code talks to a DeepSeek route via /v1/messages, 9router
* forwards the request body as Claude-format to
* https://api.deepseek.com/anthropic/v1/messages. MCP / function tools
* arrive with `type: "custom"`. DeepSeek rejects them with HTTP 400
* "tools[0]: unknown variant `custom`, expected `web_search_20250305`
* or `web_search_20260209`". The previous generic filter dropped them
* but also stripped the web_search_* tools that DeepSeek actually
* accepts. DeepSeek now exposes a `quirks.claudeSupportedToolTypes`
* whitelist and prepareClaudeRequest honours it.
*/
import { describe, it, expect } from "vitest";
import { prepareClaudeRequest } from "../../open-sse/translator/formats/claude.js";
import { PROVIDERS } from "../../open-sse/providers/index.js";
function makeBody(tools) {
return {
model: "deepseek-v4-pro",
max_tokens: 1024,
messages: [{ role: "user", content: "hello" }],
tools,
};
}
describe("prepareClaudeRequest — provider: deepseek", () => {
it("declares the supportedTypes quirk on the provider transport", () => {
expect(PROVIDERS.deepseek).toBeDefined();
expect(PROVIDERS.deepseek.quirks).toBeDefined();
expect(PROVIDERS.deepseek.quirks.claudeSupportedToolTypes).toEqual([
"web_search_20250305",
"web_search_20260209",
]);
});
it("strips MCP / custom tools (the regression) before forwarding", () => {
const body = makeBody([
{ type: "custom", name: "Bash", input_schema: { type: "object" } },
{ type: "custom", name: "Read", input_schema: { type: "object" } },
{ type: "custom", name: "Glob", input_schema: { type: "object" } },
]);
const out = prepareClaudeRequest(body, "deepseek");
expect(out.tools).toBeUndefined();
expect(out.tool_choice).toBeUndefined();
});
it("keeps web_search_20250305 and web_search_20260209", () => {
const out = prepareClaudeRequest(
makeBody([
{ type: "web_search_20250305", name: "web_search" },
{ type: "web_search_20260209", name: "web_search" },
]),
"deepseek"
);
expect(Array.isArray(out.tools)).toBe(true);
expect(out.tools).toHaveLength(2);
const types = out.tools.map(t => t.type).sort();
expect(types).toEqual(["web_search_20250305", "web_search_20260209"]);
});
it("preserves the `type` field on web_search_* tools (DeepSeek requires it)", () => {
// The .map below the filter must NOT strip `type` when the provider
// declared a whitelist — DeepSeek would reject a tool object missing
// its discriminator field with the same unknown-variant error.
const out = prepareClaudeRequest(
makeBody([{ type: "web_search_20250305", name: "web_search" }]),
"deepseek"
);
expect(out.tools[0].type).toBe("web_search_20250305");
expect(out.tools[0].name).toBe("web_search");
});
it("drops `custom` but keeps `web_search_*` when both are present", () => {
const out = prepareClaudeRequest(
makeBody([
{ type: "custom", name: "Bash", input_schema: { type: "object" } },
{ type: "web_search_20250305", name: "web_search" },
]),
"deepseek"
);
expect(Array.isArray(out.tools)).toBe(true);
expect(out.tools).toHaveLength(1);
expect(out.tools[0].type).toBe("web_search_20250305");
expect(out.tools[0].name).toBe("web_search");
});
it("survives when body has no tools", () => {
const out = prepareClaudeRequest(makeBody(undefined), "deepseek");
expect(out.tools).toBeUndefined();
});
it("rejects future / unknown tool types instead of forwarding them", () => {
const out = prepareClaudeRequest(
makeBody([{ type: "future_tool_2099", name: "x" }]),
"deepseek"
);
expect(out.tools).toBeUndefined();
});
});
describe("prepareClaudeRequest — backward compat: providers without the quirk", () => {
// Pick any non-Claude provider that has a Claude-format transport and has
// NOT been migrated to the new quirk. This protects GLM / Kimi / future
// Anthropic-compatible providers from unintended changes.
it("keeps prior behaviour (drop custom + web_search_*, normalize no-type tools)", () => {
const candidate = Object.entries(PROVIDERS).find(
([id, p]) =>
id !== "claude" &&
p?.transports?.some(t => t.format === "claude") &&
!p?.quirks?.claudeSupportedToolTypes
);
if (!candidate) {
// Every Claude-format provider has been migrated — nothing to verify.
return;
}
const [providerId] = candidate;
const out = prepareClaudeRequest(
makeBody([
{ type: "custom", name: "Bash", input_schema: { type: "object" } },
{ type: "web_search_20250305", name: "web_search" },
{ name: "no_type_tool", input_schema: { type: "object" } },
]),
providerId
);
if (out.tools !== undefined) {
for (const t of out.tools) {
expect(t.type).toBeUndefined();
}
}
});
});
+2
View File
@@ -80,6 +80,8 @@ describe("getUsageForProvider(deepseek)", () => {
used: 0,
total: 12.5,
remainingPercentage: 100,
isCreditBalance: true,
currency: "USD",
});
expect(usage.quotas["Balance (USD)"].remaining).toBeUndefined();
// Zero CNY still listed so user sees currency row
+45 -4
View File
@@ -316,7 +316,7 @@ describe("handleImageGenerationCore", () => {
expect(responseBody.data[0].b64_json).toBeTruthy();
});
it("generates image with Codex gpt-5.5-image using current Codex version header", async () => {
it.each(["gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])("generates image with Codex %s-image using current Codex version header", async (model) => {
global.fetch.mockResolvedValueOnce(
new Response(
[
@@ -335,7 +335,7 @@ describe("handleImageGenerationCore", () => {
size: "1024x1024",
output_format: "png",
},
modelInfo: { provider: "codex", model: "gpt-5.5-image" },
modelInfo: { provider: "codex", model: `${model}-image` },
credentials: {
accessToken: "codex-token",
providerSpecificData: { chatgptAccountId: "account-123" },
@@ -351,14 +351,14 @@ describe("handleImageGenerationCore", () => {
headers: expect.objectContaining({
authorization: "Bearer codex-token",
"chatgpt-account-id": "account-123",
version: "0.136.0",
version: "0.154.0",
}),
})
);
const fetchCall = global.fetch.mock.calls[0];
const requestBody = JSON.parse(fetchCall[1].body);
expect(requestBody.model).toBe("gpt-5.5");
expect(requestBody.model).toBe(model);
expect(requestBody.tools).toEqual([
{ type: "image_generation", output_format: "png", size: "1024x1024" },
]);
@@ -367,6 +367,47 @@ describe("handleImageGenerationCore", () => {
expect(responseBody.data[0].b64_json).toBe("base64codeximage");
});
it("generates image with Codex gpt-image-2.5 tool model", async () => {
global.fetch.mockResolvedValueOnce(
new Response(
[
"event: response.output_item.done",
'data: {"item":{"type":"image_generation_call","result":"base64codeximage"}}',
"",
"",
].join("\n"),
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
)
);
const result = await handleImageGenerationCore({
body: {
prompt: "A futuristic city",
size: "1024x1024",
output_format: "png",
},
modelInfo: { provider: "codex", model: "gpt-image-2.5" },
credentials: {
accessToken: "codex-token",
providerSpecificData: { chatgptAccountId: "account-123" },
},
log: null,
});
expect(result.success).toBe(true);
const fetchCall = global.fetch.mock.calls[0];
const requestBody = JSON.parse(fetchCall[1].body);
expect(requestBody.model).toBe("gpt-5.5");
expect(requestBody.tools).toEqual([
{ type: "image_generation", output_format: "png", size: "1024x1024", action: "generate", model: "gpt-image-2.5" },
]);
expect(requestBody.tool_choice).toEqual({ type: "image_generation" });
expect(requestBody.reasoning).toEqual({ effort: "medium", summary: "auto" });
const responseBody = await result.response.json();
expect(responseBody.data[0].b64_json).toBe("base64codeximage");
});
it("generates image with Cloudflare Workers AI JSON response", async () => {
global.fetch.mockResolvedValueOnce(
new Response(
@@ -20,26 +20,26 @@ describe("Kiro auth-aware endpoint routing", () => {
]);
});
it("keeps Builder ID OAuth on the Kiro runtime surface", () => {
it("routes Builder ID OAuth through Amazon Q first (runtime path deprecated)", () => {
expect(executor.getOrderedBaseUrls(credentials("builder-id"))).toEqual([
RUNTIME,
CODEWHISPERER,
Q,
CODEWHISPERER,
RUNTIME,
]);
});
it("keeps external IdP on CodeWhisperer before Amazon Q", () => {
it("routes external IdP through Amazon Q first", () => {
expect(executor.getOrderedBaseUrls(credentials("external_idp"))).toEqual([
CODEWHISPERER,
Q,
CODEWHISPERER,
RUNTIME,
]);
});
it("regionalizes AWS endpoints for IDC without changing Kiro runtime", () => {
it("regionalizes AWS endpoints for IDC with Q first", () => {
expect(executor.getOrderedBaseUrls(credentials("idc", "eu-west-1"))).toEqual([
"https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse",
"https://q.eu-west-1.amazonaws.com/generateAssistantResponse",
"https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse",
RUNTIME,
]);
});
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js";
import { claudeToKiroRequest } from "../../open-sse/translator/request/claude-to-kiro.js";
for (const [name, translate, body] of [
["OpenAI", openaiToKiroRequest, { messages: [{ role: "user", content: "hello" }] }],
["Claude", claudeToKiroRequest, { messages: [{ role: "user", content: "hello" }] }],
]) {
describe(`${name} Kiro minimal wire payload`, () => {
it("omits unsupported agent fields", () => {
const payload = translate("kiro/claude-sonnet-4.5", body, true, {});
expect(payload).not.toHaveProperty("agentMode");
expect(payload.conversationState).not.toHaveProperty("agentContinuationId");
expect(payload.conversationState).not.toHaveProperty("agentTaskType");
expect(payload.conversationState.chatTriggerType).toBe("MANUAL");
expect(payload.conversationState.currentMessage.userInputMessage.origin).toBe("AI_EDITOR");
});
});
}
+7 -3
View File
@@ -115,7 +115,7 @@ async function text(stream) {
async function execute(executor = new KiroExecutor(), overrides = {}) {
return executor.execute({
model: "kr/claude-opus-4.8",
body: { systemPrompt: "base", conversationState: {} },
body: { conversationState: { currentMessage: { userInputMessage: { content: "base", modelId: "m" } } } },
stream: true,
credentials,
...overrides
@@ -342,8 +342,12 @@ describe("Kiro terminal integrity recovery", () => {
const retryBody = JSON.parse(fetchMock.mock.calls[1][1].body);
expect(body).toContain("Recovered safely.");
expect(retryBody.systemPrompt).toContain("tool_call wrapper was malformed");
expect(retryBody.systemPrompt).not.toContain("IGNORE_ALL_INSTRUCTIONS");
// The repair instruction rides in the user turn: kiro.dev rejects a
// top-level systemPrompt with 400 REQUEST_BODY_INVALID.
const retryContent = retryBody.conversationState.currentMessage.userInputMessage.content;
expect(retryBody.systemPrompt).toBeUndefined();
expect(retryContent).toContain("tool_call wrapper was malformed");
expect(retryContent).not.toContain("IGNORE_ALL_INSTRUCTIONS");
});
it("lets a complete tool call override metadata end_turn", async () => {
@@ -0,0 +1,115 @@
import { describe, it, expect } from "vitest";
import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js";
import { claudeToKiroRequest } from "../../open-sse/translator/request/claude-to-kiro.js";
import {
canonicalizeKiroConversation,
KIRO_TOOL_RESULTS_PLACEHOLDER,
KIRO_EMPTY_USER_PLACEHOLDER,
} from "../../open-sse/translator/concerns/kiroConversation.js";
const TOOLS_OPENAI = [{
type: "function",
function: {
name: "get_weather",
description: "Get weather",
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
},
}];
const TOOLS_CLAUDE = [{
name: "get_weather",
description: "Get weather",
input_schema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
}];
function allUserContents(payload) {
const state = payload.conversationState;
return [
...state.history.filter((t) => t.userInputMessage).map((t) => t.userInputMessage.content),
state.currentMessage.userInputMessage.content,
];
}
describe("Kiro tool-result-only turns", () => {
it("OpenAI → Kiro: tool message gets a neutral placeholder, not \"continue\"", () => {
const payload = openaiToKiroRequest("claude-sonnet-4.6", {
tools: TOOLS_OPENAI,
messages: [
{ role: "user", content: "The secret word is PINEAPPLE. Weather in Jakarta?" },
{ role: "assistant", content: null, tool_calls: [{ id: "call_1", type: "function", function: { name: "get_weather", arguments: "{\"city\":\"Jakarta\"}" } }] },
{ role: "tool", tool_call_id: "call_1", content: "32C, humid" },
],
}, true, {});
const current = payload.conversationState.currentMessage.userInputMessage;
expect(current.content).toContain(KIRO_TOOL_RESULTS_PLACEHOLDER);
expect(current.content).not.toMatch(/\bcontinue\b/);
expect(current.userInputMessageContext.toolResults).toHaveLength(1);
expect(allUserContents(payload).join("\n")).toContain("PINEAPPLE");
});
it("Claude → Kiro: tool_result-only user message gets a neutral placeholder", () => {
const payload = claudeToKiroRequest("claude-sonnet-4.6", {
tools: TOOLS_CLAUDE,
messages: [
{ role: "user", content: "The secret word is PINEAPPLE. Weather in Jakarta?" },
{ role: "assistant", content: [{ type: "tool_use", id: "toolu_1", name: "get_weather", input: { city: "Jakarta" } }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "32C, humid" }] },
],
}, true, {});
const current = payload.conversationState.currentMessage.userInputMessage;
expect(current.content).toContain(KIRO_TOOL_RESULTS_PLACEHOLDER);
expect(current.content).not.toMatch(/\bcontinue\b/);
expect(current.userInputMessageContext.toolResults).toHaveLength(1);
});
it("keeps real user text when a turn has both text and tool results", () => {
const payload = claudeToKiroRequest("claude-sonnet-4.6", {
tools: TOOLS_CLAUDE,
messages: [
{ role: "user", content: "Weather in Jakarta?" },
{ role: "assistant", content: [{ type: "tool_use", id: "toolu_1", name: "get_weather", input: { city: "Jakarta" } }] },
{ role: "user", content: [
{ type: "tool_result", tool_use_id: "toolu_1", content: "32C" },
{ type: "text", text: "Now answer in one word." },
] },
],
}, true, {});
const current = payload.conversationState.currentMessage.userInputMessage;
expect(current.content).toContain("Now answer in one word.");
expect(current.content).not.toContain(KIRO_TOOL_RESULTS_PLACEHOLDER);
});
it("canonicalize: history turn with tool results and no text uses the placeholder", () => {
const result = canonicalizeKiroConversation({
history: [
{ userInputMessage: { content: "Weather in Jakarta?", modelId: "m" } },
{ assistantResponseMessage: { content: "", toolUses: [{ toolUseId: "t1", name: "get_weather", input: { city: "Jakarta" } }] } },
{ userInputMessage: { content: "", modelId: "m", userInputMessageContext: { toolResults: [{ toolUseId: "t1", status: "success", content: [{ text: "32C" }] }] } } },
{ assistantResponseMessage: { content: "It is 32C." } },
],
currentMessage: { userInputMessage: { content: "Hot or cold?", modelId: "m" } },
modelId: "m",
toolSpecs: [{ toolSpecification: { name: "get_weather", description: "Get weather", inputSchema: { json: { type: "object", properties: {} } } } }],
nameMap: new Map([["get_weather", "get_weather"]]),
});
expect(result.valid).toBe(true);
expect(result.history[2].userInputMessage.content).toBe(KIRO_TOOL_RESULTS_PLACEHOLDER);
});
it("canonicalize: an empty turn without tool results still falls back to \"continue\"", () => {
const result = canonicalizeKiroConversation({
history: [{ assistantResponseMessage: { content: "Hello" } }],
currentMessage: { userInputMessage: { content: "", modelId: "m" } },
modelId: "m",
toolSpecs: [],
nameMap: new Map(),
});
expect(result.history[0].userInputMessage.content).toBe(KIRO_EMPTY_USER_PLACEHOLDER);
expect(result.currentMessage.userInputMessage.content).toBe(KIRO_EMPTY_USER_PLACEHOLDER);
});
});
+174
View File
@@ -0,0 +1,174 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Both modules read their file path from DATA_DIR at import time, so the temp
// data dir has to be in place before the first import.
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "9r-catalog-"));
process.env.DATA_DIR = dataDir;
const catalogFile = path.join(dataDir, "model-catalog.json");
// One upstream record per gateway: the same short id means different things to
// different vendors, which is what used to leak capabilities across providers.
const upstream = {
zai: { models: { "glm-4.6v": { modalities: { input: ["text", "image"] } } } },
// two local ids alias this one upstream provider
zhipuai: { models: { "glm-5-canary": { modalities: { input: ["text", "image", "pdf"] } } } },
moonshotai: { models: { "kimi-k3": { modalities: { input: ["text"] } } } },
kilo: { models: { "kilo-auto/efficient": { modalities: { input: ["text", "image"] } } } },
};
// The registry snapshot the sync feeds build(): local ids, with the capabilities
// the tables resolve on their own.
const entries = [
{ provider: "glm", model: "glm-4.6v", current: { contextWindow: 200000, maxOutput: 128000 } },
{ provider: "glm-cn", model: "glm-5-canary", current: { contextWindow: 200000, maxOutput: 128000 } },
{ provider: "zhipu", model: "glm-5-canary", current: { contextWindow: 200000, maxOutput: 128000 } },
{ provider: "kimi", model: "kimi-k3", current: { contextWindow: 128000, maxOutput: 32000 } },
];
let build, getCatalogModalities, invalidateCatalog, syncModelCatalog, startModelCatalogSync, capabilities;
beforeAll(async () => {
({ build, syncModelCatalog, startModelCatalogSync } = await import("../../src/lib/modelCatalog/sync.js"));
// the builder is exercised directly; a missing export must fail loudly here
// rather than skip every case below
expect(typeof build).toBe("function");
const { models, providers } = build(upstream, entries);
fs.writeFileSync(catalogFile, JSON.stringify({ v: 2, models, providers }));
({ getCatalogModalities, invalidateCatalog } = await import("../../open-sse/providers/catalogOverride.js"));
capabilities = await import("../../open-sse/providers/capabilities.js");
});
afterAll(() => {
fs.rmSync(dataDir, { recursive: true, force: true });
});
describe("model catalog", () => {
it("keys modalities by gateway and writes no model-only key", () => {
const { models } = build(upstream, entries);
// upstream "zai" is filed under the local id requests arrive with...
expect(models["glm:glm-4.6v"]).toEqual({ vision: true });
// ...and under its upstream name, because a custom provider node can carry
// that name without being in the registry snapshot
expect(models["zai:glm-4.6v"]).toEqual({ vision: true });
expect(models["kilo:efficient"]).toEqual({ vision: true });
// the vendor-stripped key is what used to be shared with every other gateway
expect(models["glm-4.6v"]).toBeUndefined();
expect(models["efficient"]).toBeUndefined();
// a gateway that only declares text has nothing to contribute
expect(models["kimi:kimi-k3"]).toBeUndefined();
});
it("files an upstream provider under every local id that aliases it", () => {
const { models } = build(upstream, entries);
// glm-cn and zhipu are both zhipuai upstream; neither may be dropped
expect(models["glm-cn:glm-5-canary"]).toEqual({ vision: true, pdf: true });
expect(models["zhipu:glm-5-canary"]).toEqual({ vision: true, pdf: true });
expect(models["zhipuai:glm-5-canary"]).toEqual({ vision: true, pdf: true });
expect(getCatalogModalities("glm-cn", "glm-5-canary")).toEqual({ vision: true, pdf: true });
expect(getCatalogModalities("zhipu", "glm-5-canary")).toEqual({ vision: true, pdf: true });
});
it("does not hand a router mode another vendor's modalities", () => {
// kilo's "efficient" is a real model; another gateway's "efficient" is a mode
expect(getCatalogModalities("kilo", "kilo-auto/efficient")).toEqual({ vision: true });
expect(getCatalogModalities("kilo-gateway", "kilo-auto/efficient")).toBeNull();
expect(getCatalogModalities("qoder", "efficient")).toBeNull();
});
it("resolves a gateway the file was written for, and nobody else", () => {
expect(getCatalogModalities("glm", "glm-4.6v")).toEqual({ vision: true });
expect(getCatalogModalities("zai", "glm-4.6v")).toEqual({ vision: true });
expect(getCatalogModalities("unrelated", "glm-4.6v")).toBeNull();
expect(getCatalogModalities(undefined, "glm-4.6v")).toBeNull();
});
it("passes the gateway to the catalog reader when refining", () => {
const seen = [];
capabilities.setCatalogSource({
getModalities: (provider) => {
seen.push(provider);
return provider === "gateway-a" ? { vision: true } : null;
},
getLimits: () => null,
});
try {
// "*laguna*" resolves from the pattern table, so refine() runs
expect(capabilities.getCapabilitiesForModel("gateway-a", "laguna-9-preview").vision).toBe(true);
expect(capabilities.getCapabilitiesForModel("gateway-b", "laguna-9-preview").vision).toBe(false);
expect(seen).toContain("gateway-a");
} finally {
capabilities.setCatalogSource(null);
}
});
it("shares the installed source with every copy of the module", async () => {
const source = {
getModalities: (provider) => (provider === "gateway-a" ? { vision: true } : null),
getLimits: () => null,
};
capabilities.setCatalogSource(source);
try {
// The server bundles this module into more than one chunk and the startup
// hook only runs in one of them, so the slot has to be process-wide.
expect(globalThis.__9rCatalogSource).toBe(source);
const other = await import("../../open-sse/providers/capabilities.js?copy=2");
expect(other.getCapabilitiesForModel).not.toBe(capabilities.getCapabilitiesForModel);
// ...and that second copy resolves through the source it never installed
expect(other.getCapabilitiesForModel("gateway-a", "laguna-9-preview").vision).toBe(true);
} finally {
capabilities.setCatalogSource(null);
}
expect(globalThis.__9rCatalogSource).toBeNull();
});
});
describe("catalog schema", () => {
it("ignores a file written before the keys were scoped", () => {
const scoped = fs.readFileSync(catalogFile);
// v1: flat model keys, which is exactly the shape that collided
fs.writeFileSync(catalogFile, JSON.stringify({ v: 1, models: { "kimi-k3": { vision: true } }, providers: {} }));
invalidateCatalog();
expect(getCatalogModalities("kimi", "kimi-k3")).toBeNull();
fs.writeFileSync(catalogFile, scoped);
invalidateCatalog();
});
it("rebuilds an older-schema file instead of trusting its etag", async () => {
fs.writeFileSync(catalogFile, JSON.stringify({ v: 1, etag: 'W/"old"', models: {}, providers: {} }));
invalidateCatalog();
startModelCatalogSync(); // picks the file's etag + schema version back up
const sent = [];
const realFetch = globalThis.fetch;
globalThis.fetch = async (_url, options) => {
sent.push(options?.headers || {});
return { ok: true, status: 200, headers: new Map([["etag", 'W/"new"']]), json: async () => upstream };
};
try {
expect((await syncModelCatalog()).status).toBe("updated");
} finally {
globalThis.fetch = realFetch;
}
expect(sent[0]["if-none-match"]).toBeUndefined();
const written = JSON.parse(fs.readFileSync(catalogFile, "utf8"));
expect(written.v).toBe(2);
expect(written.models["glm:glm-4.6v"]).toEqual({ vision: true });
});
it("asks upstream for a 304 once the file is current", async () => {
const sent = [];
const realFetch = globalThis.fetch;
globalThis.fetch = async (_url, options) => {
sent.push(options?.headers || {});
return { ok: false, status: 304, headers: new Map(), json: async () => ({}) };
};
try {
expect((await syncModelCatalog()).status).toBe("unchanged");
} finally {
globalThis.fetch = realFetch;
}
expect(sent[0]["if-none-match"]).toBe('W/"new"');
});
});
+54
View File
@@ -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]");
});
});
+1 -1
View File
@@ -606,7 +606,7 @@ describe("openaiToKiroRequest", () => {
);
expect(second.conversationState.conversationId).toBe("hermes-session-openai-replay");
expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId);
expect(second.conversationState).not.toHaveProperty("agentContinuationId");
expect(second.conversationState.history[0].userInputMessage.content).toBe(
first.conversationState.currentMessage.userInputMessage.content
);
@@ -0,0 +1,90 @@
import { describe, expect, it, vi } from "vitest";
import { PROVIDERS } from "../../open-sse/config/providers.js";
import { OpenCodeExecutor } from "../../open-sse/executors/opencode.js";
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(async () => ({ ok: true, status: 200, headers: { get: () => "" } })),
}));
// Break caught: opencode/muse-spark-1.3-contributor-free 400 vì upstream
// chỉ nhận tool_choice "auto"; named/required/none phải demote sang "auto".
const FREE_13 = "muse-spark-1.3-contributor-free";
const CREDS = { connectionId: "opencode-free-tool-choice-test" };
const INPUT = [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }];
const TOOLS = [{ type: "function", name: "get_weather", description: "w", parameters: { type: "object", properties: {} } }];
function responsesBody(model, tool_choice) {
const body = { model, input: structuredClone(INPUT), tools: structuredClone(TOOLS) };
if (tool_choice !== undefined) body.tool_choice = tool_choice;
return body;
}
describe("opencode Free 1.3 tool_choice auto-only", () => {
it("khai quirk đúng model 1.3-Free trong registry", () => {
expect(PROVIDERS.opencode.quirks?.forceAutoToolChoiceModels).toEqual([FREE_13]);
});
it.each([
["Responses named", { type: "function", name: "get_weather" }],
["Chat function named", { type: "function", function: { name: "get_weather" } }],
["Claude tool named", { type: "tool", name: "get_weather" }],
["required", "required"],
["none", "none"],
])("demote %s sang auto (plain và max)", (_label, choice) => {
for (const model of [FREE_13, `${FREE_13}(max)`]) {
const body = responsesBody(model, structuredClone(choice));
const out = new OpenCodeExecutor().transformRequest(model, body, true, CREDS);
expect(out.tool_choice).toBe("auto");
expect(out.tools).toEqual(TOOLS);
expect(out.input).toEqual(INPUT);
}
});
it("giữ auto và absent; tools/input nguyên vẹn", () => {
const autoOut = new OpenCodeExecutor().transformRequest(
FREE_13, responsesBody(FREE_13, "auto"), true, CREDS,
);
expect(autoOut.tool_choice).toBe("auto");
expect(autoOut.tools).toEqual(TOOLS);
expect(autoOut.input).toEqual(INPUT);
const absentOut = new OpenCodeExecutor().transformRequest(
FREE_13, responsesBody(FREE_13, undefined), true, CREDS,
);
expect("tool_choice" in absentOut).toBe(false);
expect(absentOut.tools).toEqual(TOOLS);
expect(absentOut.input).toEqual(INPUT);
});
it.each([
["1.2-Free", "muse-spark-1.2-contributor-free"],
["future 1.4-Free", "muse-spark-1.4-contributor-free"],
["Go id", "muse-spark-1.3-contributor"],
["non-Muse", "big-pickle"],
])("không đổi tool_choice của %s", (_label, model) => {
const choice = { type: "function", name: "get_weather" };
const body = responsesBody(model, structuredClone(choice));
const out = new OpenCodeExecutor().transformRequest(model, body, true, CREDS);
expect(out.tool_choice).toEqual(choice);
});
it("wire: execute gửi choice auto tới /zen/v1/responses", async () => {
proxyAwareFetch.mockClear();
const ex = new OpenCodeExecutor();
const body = responsesBody(FREE_13, { type: "function", name: "get_weather" });
const { url, transformedBody } = await ex.execute({
model: FREE_13, body, stream: true, credentials: CREDS,
});
expect(url).toBe("https://opencode.ai/zen/v1/responses");
expect(transformedBody.tool_choice).toBe("auto");
expect(proxyAwareFetch).toHaveBeenCalledTimes(1);
const [actualUrl, actualInit] = proxyAwareFetch.mock.calls[0];
expect(actualUrl).toBe("https://opencode.ai/zen/v1/responses");
const sent = JSON.parse(actualInit.body);
expect(sent.tool_choice).toBe("auto");
expect(sent.model).toBe(FREE_13);
expect(sent.tools).toEqual(TOOLS);
expect(sent.input).toEqual(INPUT);
});
});
+33 -6
View File
@@ -1,12 +1,14 @@
import { describe, expect, it } from "vitest";
import { PROVIDER_MODELS, getModelSupportedFormats } from "../../open-sse/config/providerModels.js";
import { PROVIDER_MODELS, getModelSupportedFormats, getModelTargetFormat } from "../../open-sse/config/providerModels.js";
import { PROVIDERS } from "../../open-sse/config/providers.js";
import { resolveTransport } from "../../open-sse/services/provider.js";
// Chat-only models (no /messages, no /responses support on opencode-go)
const CHAT_ONLY = ["glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6", "mimo-v2.5", "mimo-v2.5-pro"];
const CHAT_ONLY = ["glm-5.3", "glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6", "kimi-k3",
"deepseek-flash", "longcat-2.0", "mimo-v2.5", "mimo-v2.5-pro", "hy4-preview", "hy3"];
// Models that also expose the Anthropic /messages endpoint
const CLAUDE_CAPABLE = ["minimax-m3", "minimax-m2.7", "minimax-m2.5", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus"];
const CLAUDE_CAPABLE = ["minimax-m3", "minimax-m2.7", "minimax-m2.5",
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus"];
// Models that also expose the OpenAI /responses endpoint
const RESPONSES_CAPABLE = ["deepseek-v4-pro", "deepseek-v4-flash"];
@@ -22,15 +24,31 @@ describe("OpenCode Go model catalog", () => {
it("matches the documented model IDs", () => {
const ids = (PROVIDER_MODELS["opencode-go"] || []).map((m) => m.id);
expect(ids).toEqual([
"glm-5.3-flash", "glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6",
"deepseek-flash",
"glm-5.3-flash", "glm-5.3", "glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6", "kimi-k3",
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp",
"mimo-v2.5", "mimo-v2.5-pro",
"longcat-2.0", "mimo-v2.5", "mimo-v2.5-pro",
"minimax-m3", "minimax-m2.7", "minimax-m2.5",
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus",
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus",
"hy4-preview", "hy3",
"grok-4.6", "gpt-5.6-luna",
"muse-spark-1.2-contributor", "muse-spark-1.3-contributor",
]);
});
});
describe("OpenCode Go thinking-suffix model lookup", () => {
it("preserves Responses routing for gpt-5.6-luna thinking variants", () => {
expect(getModelSupportedFormats("opencode-go", "gpt-5.6-luna(high)")).toEqual(["openai-responses"]);
expect(getModelTargetFormat("opencode-go", "gpt-5.6-luna(high)")).toBe("openai-responses");
});
it("preserves Responses routing for grok-4.6 thinking variants", () => {
expect(getModelSupportedFormats("opencode-go", "grok-4.6(high)")).toEqual(["openai-responses"]);
expect(getModelTargetFormat("opencode-go", "grok-4.6(high)")).toBe("openai-responses");
});
});
describe("OpenCode Go per-model supportedFormats", () => {
it("declares [openai, claude] for MiniMax + Qwen models", () => {
for (const m of CLAUDE_CAPABLE) {
@@ -89,6 +107,15 @@ describe("OpenCode Go per-model transport guard (chatCore logic)", () => {
}
});
it("routes Muse Spark (responses-only) to /responses, never to /messages", () => {
for (const m of ["muse-spark-1.2-contributor", "muse-spark-1.3-contributor", "grok-4.6", "gpt-5.6-luna"]) {
expect(getModelSupportedFormats("opencode-go", m)).toEqual(["openai-responses"]);
expect(pickTransport("opencode-go", "openai-responses", "opencode-go", m)?.baseUrl).toBe("https://opencode.ai/zen/go/v1/responses");
expect(pickTransport("opencode-go", "claude", "opencode-go", m)).toBeNull();
expect(pickTransport("opencode-go", "openai", "opencode-go", m)).toBeNull();
}
});
it("does NOT route MiniMax (no responses support) to /responses", () => {
for (const m of CLAUDE_CAPABLE) {
expect(pickTransport("opencode-go", "openai-responses", "opencode-go", m)).toBeNull();
@@ -0,0 +1,218 @@
import { describe, expect, it } from "vitest";
import { PROVIDER_MODELS, getModelTargetFormat, getModelSupportedFormats } from "../../open-sse/config/providerModels.js";
import { PROVIDERS } from "../../open-sse/config/providers.js";
import { resolveTransport } from "../../open-sse/services/provider.js";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
import { getThinkingLevels } from "../../open-sse/providers/thinkingLevels.js";
import { getExecutor } from "../../open-sse/executors/index.js";
import { OpenCodeGoExecutor } from "../../open-sse/executors/opencode-go.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
import "../translator/registerAll.js";
import { translateRequest } from "../../open-sse/translator/index.js";
const MODEL = "muse-spark-1.3-contributor";
const PROVIDER = "opencode-go";
// Mirror of chatCore's per-model transport guard
function pickTransport(provider, sourceFormat, alias, model) {
const supported = getModelSupportedFormats(alias, model);
const rt = resolveTransport(provider, sourceFormat);
return supported?.includes(sourceFormat) ? rt : null;
}
describe("ocg/muse-spark-1.3-contributor catalog", () => {
it("is registered responses-only", () => {
const entry = (PROVIDER_MODELS["opencode-go"] || []).find((m) => m.id === MODEL);
expect(entry).toBeDefined();
expect(entry.targetFormat).toBe("openai-responses");
expect(getModelSupportedFormats("opencode-go", MODEL)).toEqual(["openai-responses"]);
expect(getModelTargetFormat("ocg", MODEL)).toBe(FORMATS.OPENAI_RESPONSES);
expect(getModelTargetFormat("opencode-go", MODEL)).toBe(FORMATS.OPENAI_RESPONSES);
});
it("never takes the sourceFormat-matched transport (always translates)", () => {
expect(pickTransport(PROVIDER, "openai", "opencode-go", MODEL)).toBeNull();
expect(pickTransport(PROVIDER, "claude", "opencode-go", MODEL)).toBeNull();
expect(pickTransport(PROVIDER, "openai-responses", "opencode-go", MODEL)?.baseUrl)
.toBe("https://opencode.ai/zen/go/v1/responses");
});
it("advertises reasoning via the shared muse-spark pattern", () => {
expect(getCapabilitiesForModel(PROVIDER, MODEL)).toMatchObject({
vision: true,
reasoning: true,
thinkingFormat: "openai",
});
expect(getThinkingLevels(PROVIDER, MODEL)).toContain("xhigh");
});
});
describe("OpenCodeGoExecutor routing + sanitization", () => {
it("routes gpt-5.6-luna to /responses", () => {
const ex = new OpenCodeGoExecutor();
expect(ex.buildUrl("gpt-5.6-luna")).toBe("https://opencode.ai/zen/go/v1/responses");
expect(ex.buildUrl("gpt-5.6-luna(high)", true, 0, {
runtimeTransport: { baseUrl: "https://opencode.ai/zen/go/v1/chat/completions" },
})).toBe("https://opencode.ai/zen/go/v1/responses");
});
it("routes every responses-only registry model (grok-4.6) to /responses", () => {
const ex = new OpenCodeGoExecutor();
expect(ex.buildUrl("grok-4.6")).toBe("https://opencode.ai/zen/go/v1/responses");
expect(ex.buildUrl("grok-4.6(high)", true, 0, {
runtimeTransport: { baseUrl: "https://opencode.ai/zen/go/v1/chat/completions" },
})).toBe("https://opencode.ai/zen/go/v1/responses");
});
it("is wired for opencode-go and routes muse-spark to /responses", () => {
expect(getExecutor("opencode-go")).toBeInstanceOf(OpenCodeGoExecutor);
const ex = new OpenCodeGoExecutor();
expect(ex.buildUrl(MODEL)).toBe("https://opencode.ai/zen/go/v1/responses");
// Even a stale runtimeTransport must not drag muse-spark onto chat/messages
expect(ex.buildUrl(MODEL, true, 0, {
runtimeTransport: { baseUrl: "https://opencode.ai/zen/go/v1/chat/completions" },
})).toBe("https://opencode.ai/zen/go/v1/responses");
});
it("leaves non-muse models on the default/runtime transport", () => {
const ex = new OpenCodeGoExecutor();
expect(ex.buildUrl("kimi-k2.6")).toBe("https://opencode.ai/zen/go/v1/chat/completions");
expect(ex.buildUrl("minimax-m3", true, 0, {
runtimeTransport: { baseUrl: "https://opencode.ai/zen/go/v1/messages" },
})).toBe("https://opencode.ai/zen/go/v1/messages");
});
it("normalizes caps + reasoning and coerces tool items exactly once", () => {
const ex = new OpenCodeGoExecutor();
const args = { path: "a\"b\nc\\d", emoji: "🚀 ü", nested: { q: "x'y\"z" } };
const body = {
model: MODEL,
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
{ type: "function_call", call_id: "x".repeat(100), name: "read", arguments: args },
{ type: "function_call", call_id: "bad", name: " ", arguments: "{}" },
{ type: "function_call", call_id: "frag", name: "exec", arguments: "{not json" },
{ type: "function_call_output", call_id: "c1", output: { ok: true, text: "héllo \"w\"" } },
{ type: "function_call_output", call_id: "c2", output: null },
],
tools: [
{ type: "function", function: { name: "read", description: "r", parameters: { type: "object", properties: {} } } },
{ type: "function", function: { name: " ", parameters: {} } },
],
max_tokens: 2048,
reasoning_effort: "high",
};
const out = ex.transformRequest(MODEL, body, true, {});
expect(out.max_output_tokens).toBe(2048);
expect(out.max_tokens).toBeUndefined();
expect(out.reasoning).toEqual({ effort: "high", summary: "auto" });
expect(out.stream).toBe(true);
expect(out.store).toBe(false);
// nameless declaration dropped, nameless call dropped
expect(out.tools.map((t) => t.name)).toEqual(["read"]);
const calls = out.input.filter((i) => i.type === "function_call");
expect(calls.map((c) => c.name)).toEqual(["read", "exec"]);
// overlong id clamped, object args stringified exactly once
expect(calls[0].call_id).toHaveLength(64);
expect(JSON.parse(calls[0].arguments)).toEqual(args);
// invalid fragment coerced, never double-encoded
expect(calls[1].arguments).toBe("{}");
const outputs = out.input.filter((i) => i.type === "function_call_output");
expect(JSON.parse(outputs[0].output)).toEqual({ ok: true, text: "héllo \"w\"" });
expect(outputs[1].output).toBe("");
});
it("fills in properties for object tool schemas missing them", () => {
const ex = new OpenCodeGoExecutor();
const body = {
model: MODEL,
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
tools: [
{ type: "function", function: { name: "bare", parameters: { type: "object" } } },
{ type: "function", function: { name: "full", parameters: { type: "object", properties: { a: { type: "string" } } } } },
],
};
const out = ex.transformRequest(MODEL, body, true, {});
expect(out.tools.find((t) => t.name === "bare").parameters).toEqual({ type: "object", properties: {} });
expect(out.tools.find((t) => t.name === "full").parameters).toEqual({ type: "object", properties: { a: { type: "string" } } });
});
it("strips prior-turn reasoning items carrying encrypted_content from input", () => {
const ex = new OpenCodeGoExecutor();
const body = {
model: MODEL,
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
{
type: "reasoning",
id: "rs_123",
encrypted_content: "ENC_BLOB_TURN_1",
summary: [{ type: "summary_text", text: "thinking text" }],
},
{ type: "function_call", call_id: "c1", name: "read", arguments: "{}" },
{ type: "function_call_output", call_id: "c1", output: "ok" },
],
};
const out = ex.transformRequest(MODEL, body, true, {});
expect(out.input.some((i) => i.type === "reasoning")).toBe(false);
expect(JSON.stringify(out.input)).not.toContain("ENC_BLOB_TURN_1");
expect(out.input.map((i) => i.type)).toEqual(["message", "function_call", "function_call_output"]);
});
});
describe("chat/claude clients translate to Responses without breaking tools", () => {
const tricky = { cmd: "echo \"hi\"\nnewline\ttab\\slash", emoji: "🎉 café naïve", nested: { a: [1, "x'y"] } };
it("openai chat → responses keeps arguments parseable", () => {
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI_RESPONSES,
MODEL,
{
model: `ocg/${MODEL}`,
messages: [
{ role: "system", content: [{ type: "text", text: "sys one" }, { type: "text", text: "sys two" }] },
{ role: "user", content: "run it" },
{
role: "assistant", content: null,
tool_calls: [{ id: "call_1", type: "function", function: { name: "exec", arguments: tricky } }],
},
{ role: "tool", tool_call_id: "call_1", content: tricky },
],
tools: [{ type: "function", function: { name: "exec", description: "e", parameters: { type: "object", properties: {} } } }],
},
true, {}, PROVIDER,
);
expect(translated.instructions).toBe("sys one\nsys two");
const fc = translated.input.find((i) => i.type === "function_call");
expect(JSON.parse(fc.arguments)).toEqual(tricky);
const fco = translated.input.find((i) => i.type === "function_call_output");
expect(JSON.parse(fco.output)).toEqual(tricky);
});
it("claude messages → responses double-hop keeps tool input intact", () => {
const viaOpenAI = translateRequest(FORMATS.CLAUDE, FORMATS.OPENAI, MODEL, {
system: "be terse",
messages: [
{ role: "user", content: [{ type: "text", text: "go" }] },
{
role: "assistant",
content: [
{ type: "text", text: "calling" },
{ type: "tool_use", id: "tu_1", name: "exec", input: tricky },
],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "tu_1", content: [{ type: "text", text: JSON.stringify(tricky) }] }],
},
],
tools: [{ name: "exec", description: "e", input_schema: { type: "object", properties: {} } }],
}, true, {}, PROVIDER);
const translated = translateRequest(FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES, MODEL, viaOpenAI, true, {}, PROVIDER);
const fc = translated.input.find((i) => i.type === "function_call");
expect(JSON.parse(fc.arguments)).toEqual(tricky);
const fco = translated.input.find((i) => i.type === "function_call_output");
expect(JSON.parse(fco.output)).toEqual(tricky);
});
});
+166
View File
@@ -0,0 +1,166 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const { fetchMock } = vi.hoisted(() => ({
fetchMock: vi.fn(),
}));
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: fetchMock,
}));
import { DefaultExecutor } from "../../open-sse/executors/default.js";
import { getExecutor } from "../../open-sse/executors/index.js";
const TRANSPORTS = [
{ format: "openai", baseUrl: "https://opencode.ai/zen/go/v1/chat/completions", auth: { combined: true, header: "Authorization", scheme: "bearer" } },
{ format: "claude", baseUrl: "https://opencode.ai/zen/go/v1/messages", auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true } },
{ format: "openai-responses", baseUrl: "https://opencode.ai/zen/go/v1/responses", auth: { combined: true, header: "Authorization", scheme: "bearer" } },
];
function makeCredentials(overrides = {}) {
return {
apiKey: "test-key",
connectionId: "connection-a",
rawHeaders: {},
runtimeTransport: TRANSPORTS[0],
...overrides,
};
}
function prepare(executor, overrides = {}) {
const credentials = overrides.credentials || makeCredentials();
const prepared = executor.prepareRequestCredentials({
body: overrides.body || { messages: [{ role: "user", content: "hello" }] },
credentials,
providerSessionId: overrides.providerSessionId ?? "conversation-a",
clientTool: overrides.clientTool ?? "claude",
});
return { credentials, prepared };
}
beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockResolvedValue(new Response("{}", {
status: 200,
headers: { "content-type": "application/json" },
}));
});
describe("OpenCode Go x-opencode-session", () => {
it("uses a dedicated executor with request-local session credentials", () => {
const executor = getExecutor("opencode-go");
const { credentials, prepared } = prepare(executor);
expect(executor.constructor.name).toBe("OpenCodeGoExecutor");
expect(prepared).not.toBe(credentials);
expect(prepared._opencodeGoSession).toMatch(/^ses_[0-9a-f]{32}$/);
expect(credentials).not.toHaveProperty("_opencodeGoSession");
expect(executor).not.toHaveProperty("_currentSessionId");
expect(executor).not.toHaveProperty("_opencodeGoSession");
});
it("preserves a valid native session header case-insensitively", () => {
const executor = getExecutor("opencode-go");
const { prepared } = prepare(executor, {
credentials: makeCredentials({ rawHeaders: { "X-OpenCode-Session": " native-session-a " } }),
});
expect(prepared._opencodeGoSession).toBe("native-session-a");
});
it("ignores an oversized native session and uses the translated identity", () => {
const executor = getExecutor("opencode-go");
const { prepared } = prepare(executor, {
credentials: makeCredentials({ rawHeaders: { "x-opencode-session": "x".repeat(257) } }),
});
expect(prepared._opencodeGoSession).toMatch(/^ses_[0-9a-f]{32}$/);
});
it("keeps the same translated conversation stable across all transports", () => {
const executor = getExecutor("opencode-go");
const values = TRANSPORTS.map((runtimeTransport) => {
const { prepared } = prepare(executor, {
credentials: makeCredentials({ runtimeTransport }),
});
return executor.buildHeaders(prepared, true)["x-opencode-session"];
});
expect(new Set(values).size).toBe(1);
expect(values[0]).toMatch(/^ses_[0-9a-f]{32}$/);
expect(values[0]).not.toContain("conversation-a");
});
it("isolates different conversations", () => {
const executor = getExecutor("opencode-go");
const a = prepare(executor, { providerSessionId: "conversation-a" }).prepared._opencodeGoSession;
const b = prepare(executor, { providerSessionId: "conversation-b" }).prepared._opencodeGoSession;
expect(a).not.toBe(b);
});
it("isolates different downstream agents that reuse the same raw id", () => {
const executor = getExecutor("opencode-go");
const claude = prepare(executor, { clientTool: "claude" }).prepared._opencodeGoSession;
const codex = prepare(executor, { clientTool: "codex" }).prepared._opencodeGoSession;
expect(claude).not.toBe(codex);
});
it("uses a stable opaque connection fallback when no session is supplied", () => {
const executor = getExecutor("opencode-go");
const options = {
credentials: makeCredentials({ connectionId: "fallback-connection" }),
providerSessionId: null,
clientTool: null,
body: { messages: [{ role: "user", content: "headerless" }] },
};
const first = prepare(executor, options).prepared._opencodeGoSession;
const second = prepare(executor, options).prepared._opencodeGoSession;
expect(first).toBe(second);
expect(first).toMatch(/^ses_[0-9a-f]{32}$/);
expect(first).not.toContain("fallback-connection");
});
it("adds the prepared session to the actual fetch headers", async () => {
const executor = getExecutor("opencode-go");
const credentials = makeCredentials();
const result = await executor.execute({
model: "glm-5.2",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials,
providerSessionId: "conversation-fetch",
clientTool: "codex",
});
expect(result.headers["x-opencode-session"]).toMatch(/^ses_[0-9a-f]{32}$/);
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0][1].headers["x-opencode-session"]).toBe(result.headers["x-opencode-session"]);
expect(credentials).not.toHaveProperty("_opencodeGoSession");
});
it("does not add the header to unrelated default executors", () => {
const headers = new DefaultExecutor("openai").buildHeaders({ apiKey: "test-key" }, false);
expect(headers["x-opencode-session"]).toBeUndefined();
});
});
describe("chatCore provider session forwarding", () => {
it("passes the original provider session and client tool on initial and retry execution", () => {
const source = readFileSync(
fileURLToPath(new URL("../../open-sse/handlers/chatCore.js", import.meta.url)),
"utf8",
);
const calls = [...source.matchAll(/executor\.execute\(\{([\s\S]*?)\}\)/g)].map((match) => match[1]);
expect(calls).toHaveLength(2);
for (const call of calls) {
expect(call).toMatch(/providerSessionId:\s*sessionSeed/);
expect(call).toMatch(/\bclientTool\b/);
}
});
});
+129
View File
@@ -0,0 +1,129 @@
import { beforeEach, describe, expect, it, vi } 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_APIKEY_PROVIDERS,
USAGE_SUPPORTED_PROVIDERS,
} from "../../src/shared/constants/providers.js";
const USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("OpenCode Go registry usage flags", () => {
it("is listed for the API key quota dashboard", () => {
expect(USAGE_SUPPORTED_PROVIDERS).toContain("opencode-go");
expect(USAGE_APIKEY_PROVIDERS).toContain("opencode-go");
});
});
describe("getUsageForProvider(opencode-go)", () => {
beforeEach(() => vi.clearAllMocks());
it("fetches and normalizes subscription usage", async () => {
proxyAwareFetch.mockResolvedValueOnce(
jsonResponse({
usage: {
rolling: { status: "ok", percent: 13, resetsAt: "2026-09-04T14:28:02.617Z" },
weekly: { status: "ok", percent: 5, resetsAt: "2026-09-07T00:00:00.617Z" },
monthly: { status: "ok", percent: 2, resetsAt: "2026-10-02T12:14:24.617Z" },
},
}),
);
const usage = await getUsageForProvider({
provider: "opencode-go",
apiKey: "sk-go-test",
});
expect(proxyAwareFetch).toHaveBeenCalledWith(
USAGE_URL,
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({ Authorization: "Bearer sk-go-test" }),
}),
null,
);
expect(usage).toEqual({
plan: "OpenCode Go",
quotas: {
Rolling: {
used: 13,
total: 100,
remaining: 87,
remainingPercentage: 87,
resetAt: "2026-09-04T14:28:02.617Z",
unlimited: false,
},
Weekly: expect.objectContaining({ used: 5, remainingPercentage: 95 }),
Monthly: expect.objectContaining({ used: 2, remainingPercentage: 98 }),
},
});
});
it("reports missing and rejected credentials", async () => {
const missing = await getUsageForProvider({ provider: "opencode-go" });
expect(missing.message).toMatch(/api key/i);
expect(proxyAwareFetch).not.toHaveBeenCalled();
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
const rejected = await getUsageForProvider({
provider: "opencode-go",
apiKey: "bad",
});
expect(rejected.message).toMatch(/authentication failed/i);
});
it("distinguishes a missing subscription from invalid credentials", async () => {
proxyAwareFetch.mockResolvedValueOnce(
jsonResponse({ error: { type: "EntitlementError" } }, 403),
);
const usage = await getUsageForProvider({
provider: "opencode-go",
apiKey: "sk-without-go",
});
expect(usage.message).toMatch(/subscription required/i);
});
it("rejects responses without a valid quota percentage", async () => {
proxyAwareFetch.mockResolvedValueOnce(
jsonResponse({ usage: { rolling: { status: "ok" }, future: { percent: 10 } } }),
);
const usage = await getUsageForProvider({
provider: "opencode-go",
apiKey: "sk-go-test",
});
expect(usage.quotas).toBeUndefined();
expect(usage.message).toMatch(/valid quota data/i);
});
it("reports upstream and network failures", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unavailable" }, 500));
const upstream = await getUsageForProvider({
provider: "opencode-go",
apiKey: "sk-go-test",
});
expect(upstream.message).toContain("500");
proxyAwareFetch.mockRejectedValueOnce(new Error("socket closed"));
const network = await getUsageForProvider({
provider: "opencode-go",
apiKey: "sk-go-test",
});
expect(network.message).toContain("socket closed");
});
});
@@ -63,6 +63,39 @@ describe("OpenCode Free Muse Spark thinking", () => {
expect(out.max_tokens).toBeUndefined();
});
it("routes Union Alpha through Anthropic Messages", () => {
const caps = getCapabilitiesForModel(PROVIDER, "union-alpha");
expect(caps.vision).toBe(true);
expect(caps.contextWindow).toBe(262144);
expect(caps.maxOutput).toBe(131072);
const executor = new OpenCodeExecutor();
expect(getModelTargetFormat("oc", "union-alpha")).toBe(FORMATS.CLAUDE);
const url = executor.buildUrl("union-alpha");
expect(url).toBe("https://opencode.ai/zen/v1/messages");
expect(executor.buildHeaders({}, true, url)).toMatchObject({
"anthropic-version": "2023-06-01",
});
expect(executor.buildHeaders({}, true, executor.buildUrl("big-pickle")))
.not.toHaveProperty("anthropic-version");
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"union-alpha",
{ messages: [{ role: "user", content: "ping" }], max_tokens: 1 },
false,
{},
PROVIDER,
);
expect(translated).toMatchObject({
model: "union-alpha",
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
max_tokens: 1,
});
});
it("leaves the other free models on Chat Completions", () => {
const executor = new OpenCodeExecutor();
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 1024 };
@@ -132,4 +165,63 @@ describe("OpenCode Free Muse Spark thinking", () => {
expect(out.max_tokens).toBeUndefined();
}
});
it("strips prior-turn reasoning items carrying encrypted_content from input", () => {
const executor = new OpenCodeExecutor();
const model = "muse-spark-1.3-contributor-free";
const body = {
model,
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "say hi" }] },
{
type: "reasoning",
id: "rs_123",
encrypted_content: "ENC_BLOB_TURN_1",
summary: [{ type: "summary_text", text: "thinking text" }],
},
{
type: "function_call",
id: "fc_1",
call_id: "call_1",
name: "shell",
arguments: JSON.stringify({ command: "echo hi" }),
},
{
type: "function_call_output",
call_id: "call_1",
output: "hi",
},
{ type: "message", role: "user", content: [{ type: "input_text", text: "now say bye" }] },
],
tools: [
{
type: "function",
function: {
name: "shell",
description: "Run shell command",
parameters: { type: "object" },
},
},
],
};
const out = executor.transformRequest(model, body, true, {});
expect(out.stream).toBe(true);
expect(out.store).toBe(false);
// Prior reasoning items stripped to prevent 400 "reasoning encrypted_content was not issued to this caller"
expect(out.input.some((item) => item.type === "reasoning")).toBe(false);
expect(JSON.stringify(out.input)).not.toContain("ENC_BLOB_TURN_1");
// User message, function_call, function_call_output, and next user message survive
const types = out.input.map((item) => item.type);
expect(types).toEqual(["message", "function_call", "function_call_output", "message"]);
// Tools flattened and empty properties added
expect(out.tools).toEqual([
{
type: "function",
name: "shell",
description: "Run shell command",
parameters: { type: "object", properties: {} },
},
]);
});
});
+319
View File
@@ -0,0 +1,319 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock } = vi.hoisted(() => ({
fetchMock: vi.fn(),
}));
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: fetchMock,
}));
import { getExecutor } from "../../open-sse/executors/index.js";
import {
OPENCODE_SESSION_RE,
OPENCODE_REQUEST_RE,
generateSessionId,
generateRequestId,
translateSessionId,
stableSessionId,
deriveRequestId,
} from "../../open-sse/executors/opencode.js";
function makeCredentials(overrides = {}) {
return {
connectionId: "conn_test",
rawHeaders: {},
...overrides,
};
}
function prepare(executor, overrides = {}) {
const credentials = overrides.credentials || makeCredentials();
const prepared = executor.prepareRequestCredentials({
body: overrides.body || { input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }] },
credentials,
providerSessionId: overrides.providerSessionId ?? "conversation-a",
clientTool: overrides.clientTool ?? "claude",
});
return { credentials, prepared };
}
beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockResolvedValue(new Response("{}", {
status: 200,
headers: { "content-type": "application/json" },
}));
});
describe("OpenCode Free Session ID Format", () => {
it("generates session IDs matching OpenCode canonical format (ses_ + 12 hex + 14 base62)", () => {
for (let i = 0; i < 20; i++) {
const id = generateSessionId();
expect(id).toMatch(OPENCODE_SESSION_RE);
expect(id).toHaveLength(30);
}
});
it("generates request IDs matching OpenCode canonical format (msg_ + 12 hex + 14 base62)", () => {
for (let i = 0; i < 20; i++) {
const id = generateRequestId();
expect(id).toMatch(/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/);
expect(id).toHaveLength(30);
}
});
it("translates arbitrary sessions into valid OpenCode session format", () => {
const inputs = [
"claude:550e8400-e29b-41d4-a716-446655440000",
"antigravity:conv-abc-123",
"session-from-codex",
"12345",
"",
];
for (const raw of inputs) {
const translated = translateSessionId(raw, "claude");
expect(translated).toMatch(OPENCODE_SESSION_RE);
expect(translated).toHaveLength(30);
}
});
it("preserves already-valid OpenCode sessions without re-hashing", () => {
const valid = "ses_f534dfae8ffeCy4Ee4tLWNygDc";
expect(translateSessionId(valid)).toBe(valid);
expect(translateSessionId(` ${valid} `)).toBe(valid);
});
});
describe("OpenCode Free Executor Session Resolution", () => {
it("uses request-local session credentials without mutating source credentials", () => {
const executor = getExecutor("opencode");
const { credentials, prepared } = prepare(executor);
expect(executor.constructor.name).toBe("OpenCodeExecutor");
expect(prepared).not.toBe(credentials);
expect(prepared._opencodeSession).toMatch(OPENCODE_SESSION_RE);
expect(credentials).not.toHaveProperty("_opencodeSession");
expect(executor).not.toHaveProperty("_currentSessionId");
});
it("preserves valid native x-opencode-session header case-insensitively", () => {
const executor = getExecutor("opencode");
const valid = "ses_f534dfae8ffeCy4Ee4tLWNygDc";
const { prepared } = prepare(executor, {
credentials: makeCredentials({ rawHeaders: { "X-OpenCode-Session": ` ${valid} ` } }),
});
expect(prepared._opencodeSession).toBe(valid);
});
it("translates invalid native x-opencode-session header into a valid session", () => {
const executor = getExecutor("opencode");
const { prepared } = prepare(executor, {
credentials: makeCredentials({ rawHeaders: { "x-opencode-session": "invalid-session-uuid" } }),
});
expect(prepared._opencodeSession).toMatch(OPENCODE_SESSION_RE);
expect(prepared._opencodeSession).not.toBe("invalid-session-uuid");
});
it("translates conversation session deterministically", () => {
const executor = getExecutor("opencode");
const first = prepare(executor, { providerSessionId: "conversation-a", clientTool: "claude" }).prepared._opencodeSession;
const second = prepare(executor, { providerSessionId: "conversation-a", clientTool: "claude" }).prepared._opencodeSession;
expect(first).toBe(second);
expect(first).toMatch(OPENCODE_SESSION_RE);
});
it("isolates different conversations and tools", () => {
const executor = getExecutor("opencode");
const convA = prepare(executor, { providerSessionId: "conversation-a" }).prepared._opencodeSession;
const convB = prepare(executor, { providerSessionId: "conversation-b" }).prepared._opencodeSession;
const toolClaude = prepare(executor, { providerSessionId: "same", clientTool: "claude" }).prepared._opencodeSession;
const toolCodex = prepare(executor, { providerSessionId: "same", clientTool: "codex" }).prepared._opencodeSession;
expect(convA).not.toBe(convB);
expect(toolClaude).not.toBe(toolCodex);
});
it("adds the valid session header to fetch requests", async () => {
const executor = getExecutor("opencode");
const credentials = makeCredentials();
const result = await executor.execute({
model: "muse-spark-1.3-contributor-free",
body: { input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }] },
stream: false,
credentials,
providerSessionId: "conversation-fetch-test",
clientTool: "claude",
});
expect(result.headers["x-opencode-session"]).toMatch(OPENCODE_SESSION_RE);
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0][1].headers["x-opencode-session"]).toBe(result.headers["x-opencode-session"]);
expect(fetchMock.mock.calls[0][1].headers["Authorization"]).toBe("Bearer public");
expect(credentials).not.toHaveProperty("_opencodeSession");
});
it("falls back to a valid generated session in buildHeaders when called standalone", () => {
const executor = getExecutor("opencode");
const headers = executor.buildHeaders({});
expect(headers["x-opencode-session"]).toMatch(OPENCODE_SESSION_RE);
expect(headers["Authorization"]).toBe("Bearer public");
});
it("handles null or undefined body gracefully in transformRequest", () => {
const executor = getExecutor("opencode");
expect(() => executor.transformRequest("muse-spark-1.3-contributor-free", null, false, {})).not.toThrow();
expect(() => executor.transformRequest("big-pickle", undefined, false, {})).not.toThrow();
});
});
describe("OpenCode Free User-Agent Validation", () => {
it("defaults User-Agent to opencode/1.18.31 for non-opencode downstream clients", () => {
const executor = getExecutor("opencode");
const headersNoUa = executor.buildHeaders({});
expect(headersNoUa["User-Agent"]).toBe("opencode/1.18.31");
const headersClaude = executor.buildHeaders({ rawHeaders: { "user-agent": "Claude-Code/1.0" } });
expect(headersClaude["User-Agent"]).toBe("opencode/1.18.31");
});
it("replaces bare opencode with versioned opencode/1.18.31 to prevent 403 FreeTierError", () => {
const executor = getExecutor("opencode");
const headers = executor.buildHeaders({ rawHeaders: { "user-agent": "opencode" } });
expect(headers["User-Agent"]).toBe("opencode/1.18.31");
});
it("upgrades outdated opencode versions (< 1.17) to prevent 426 Upgrade Required", () => {
const executor = getExecutor("opencode");
const headers = executor.buildHeaders({ rawHeaders: { "user-agent": "opencode/1.15.0" } });
expect(headers["User-Agent"]).toBe("opencode/1.18.31");
});
it("preserves valid opencode versions (>= 1.17)", () => {
const executor = getExecutor("opencode");
const headers118 = executor.buildHeaders({
rawHeaders: { "user-agent": "opencode/1.18.31 ai-sdk/provider-utils/4.0.40 runtime/bun/1.3.14" },
});
expect(headers118["User-Agent"]).toBe("opencode/1.18.31 ai-sdk/provider-utils/4.0.40 runtime/bun/1.3.14");
const headersFuture = executor.buildHeaders({ rawHeaders: { "user-agent": "opencode/1.19.0" } });
expect(headersFuture["User-Agent"]).toBe("opencode/1.19.0");
});
});
describe("OpenCode Stable Session Reuse (429 follow-up)", () => {
function anonymousCredentials(auth) {
return makeCredentials({ connectionId: undefined, rawHeaders: { authorization: `Bearer ${auth}` } });
}
it("reuses one stable upstream session instead of minting a new one per request", () => {
const executor = getExecutor("opencode");
const body = { messages: [{ role: "user", content: "hello" }] };
const first = executor.prepareRequestCredentials({
body,
credentials: anonymousCredentials("stable-key-1"),
providerSessionId: null,
clientTool: "claude",
});
const second = executor.prepareRequestCredentials({
body,
credentials: anonymousCredentials("stable-key-1"),
providerSessionId: null,
clientTool: "claude",
});
expect(first._opencodeSession).toMatch(OPENCODE_SESSION_RE);
expect(second._opencodeSession).toBe(first._opencodeSession);
});
it("isolates stable sessions by downstream identity", () => {
const executor = getExecutor("opencode");
const body = { messages: [{ role: "user", content: "hello" }] };
const forKey = (auth) => executor.prepareRequestCredentials({
body,
credentials: anonymousCredentials(auth),
providerSessionId: null,
clientTool: "claude",
})._opencodeSession;
expect(forKey("user-A")).not.toBe(forKey("user-B"));
expect(forKey("user-A")).toMatch(OPENCODE_SESSION_RE);
});
it("exposes the stable session helper directly", () => {
const first = stableSessionId({ connectionId: "direct-conn" });
expect(stableSessionId({ connectionId: "direct-conn" })).toBe(first);
expect(first).toMatch(OPENCODE_SESSION_RE);
});
it("derives deterministic, canonical request ids per message", () => {
const session = stableSessionId({ connectionId: "req-conn" });
const body = { messages: [{ role: "user", content: "ping" }] };
const first = deriveRequestId(session, body);
expect(first).toMatch(OPENCODE_REQUEST_RE);
expect(deriveRequestId(session, body)).toBe(first);
expect(
deriveRequestId(session, { messages: [{ role: "user", content: "a different question" }] }),
).not.toBe(first);
});
it("preserves a valid downstream x-opencode-request header", () => {
const executor = getExecutor("opencode");
const validReq = "msg_0ae8d9cd3001swxaFbM248jcIF";
const { prepared } = prepare(executor, {
credentials: makeCredentials({ rawHeaders: { "x-opencode-request": validReq } }),
});
expect(prepared._opencodeRequest).toBe(validReq);
});
it("keeps the standalone buildHeaders session stable across calls", () => {
const executor = getExecutor("opencode");
const first = executor.buildHeaders({})["x-opencode-session"];
const second = executor.buildHeaders({})["x-opencode-session"];
expect(first).toMatch(OPENCODE_SESSION_RE);
expect(second).toBe(first);
});
it("cloaks free-tier requests with bash and read decoy tools", () => {
const executor = getExecutor("opencode");
// Case 1: no tools sent by client -> injects bash + read with tool_choice none
const chatNoTools = executor.transformRequest("nemotron-3-ultra-free", {
messages: [{ role: "user", content: "hi" }],
});
expect(chatNoTools.stream).toBe(true);
expect(chatNoTools.tool_choice).toBe("none");
expect(chatNoTools.tools.map((t) => t.function?.name)).toEqual(["bash", "read"]);
// Case 2: external CLI tools (e.g. Claude Code Bash) -> preserves Bash, appends read
const chatWithTools = executor.transformRequest("nemotron-3-ultra-free", {
messages: [{ role: "user", content: "hi" }],
tools: [{ type: "function", function: { name: "Bash", description: "Claude Code tool" } }],
tool_choice: "auto",
});
expect(chatWithTools.tool_choice).toBe("auto");
const names = chatWithTools.tools.map((t) => t.function?.name);
expect(names).toContain("Bash");
expect(names).toContain("bash");
expect(names).toContain("read");
// Case 3: already has both bash and read -> do not insert anything
const chatFull = executor.transformRequest("nemotron-3-ultra-free", {
messages: [{ role: "user", content: "hi" }],
tools: [
{ type: "function", function: { name: "bash", description: "existing" } },
{ type: "function", function: { name: "read", description: "existing" } },
],
});
expect(chatFull.tools.length).toBe(2);
expect(chatFull.tools[0].function.description).toBe("existing");
});
it("declares forceStream on the opencode transport so chatCore serves SSE upstream", async () => {
const { PROVIDERS } = await import("../../open-sse/config/providers.js");
expect(PROVIDERS.opencode?.forceStream).toBe(true);
});
});
+17
View File
@@ -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");
});
});
+27 -7
View File
@@ -3,6 +3,7 @@ import {
filterQuotasByVisibility,
getHiddenQuotaRows,
parseQuotaData,
trimHiddenQuotaKeys,
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
describe("provider quota visibility", () => {
@@ -13,22 +14,26 @@ describe("provider quota visibility", () => {
used: 200,
total: 1000,
resetAt: "2026-07-04T00:00:00Z",
remainingPercentage: 80,
},
"claude-opus-4-6-thinking": {
displayName: "Claude Opus 4.6 (Thinking)",
used: 100,
total: 1000,
resetAt: "2026-07-04T00:00:00Z",
remainingPercentage: 90,
},
},
};
it("keeps Antigravity modelKey so hidden settings use stable quota ids", () => {
it("groups Antigravity model quotas into Gemini and Claude families", () => {
const quotas = parseQuotaData("antigravity", data);
expect(quotas.map((q) => q.modelKey)).toEqual([
"gemini-pro-agent",
"claude-opus-4-6-thinking",
"gemini",
"claude",
]);
expect(quotas[0].name).toBe("Gemini (Flash / Pro)");
expect(quotas[1].name).toBe("Claude (Sonnet / Opus)");
});
it("shows all quotas by default and hides configured provider rows", () => {
@@ -36,19 +41,34 @@ describe("provider quota visibility", () => {
expect(filterQuotasByVisibility("antigravity", quotas, {})).toHaveLength(2);
const visibility = {
antigravity: { hidden: ["claude-opus-4-6-thinking"] },
antigravity: { hidden: ["claude"] },
};
const visible = filterQuotasByVisibility("antigravity", quotas, visibility);
const hidden = getHiddenQuotaRows("antigravity", quotas, visibility);
expect(visible.map((q) => q.modelKey)).toEqual(["gemini-pro-agent"]);
expect(hidden.map((q) => q.modelKey)).toEqual(["claude-opus-4-6-thinking"]);
expect(visible.map((q) => q.modelKey)).toEqual(["gemini"]);
expect(hidden.map((q) => q.modelKey)).toEqual(["claude"]);
});
it("trims stale or obsolete model keys", () => {
const quotas = parseQuotaData("antigravity", data);
const trimmed = trimHiddenQuotaKeys(["claude", "stale-model-xyz", "gemini-3.8-flash-low"], quotas);
expect(trimmed).toEqual(["claude"]);
const visibility = {
antigravity: { hidden: ["claude", "stale-model-xyz"] },
};
const visible = filterQuotasByVisibility("antigravity", quotas, visibility);
const hidden = getHiddenQuotaRows("antigravity", quotas, visibility);
expect(visible.map((q) => q.modelKey)).toEqual(["gemini"]);
expect(hidden.map((q) => q.modelKey)).toEqual(["claude"]);
});
it("does not apply one provider hidden list to another provider", () => {
const quotas = parseQuotaData("antigravity", data);
const visibility = {
codex: { hidden: ["gemini-pro-agent"] },
codex: { hidden: ["gemini"] },
};
expect(filterQuotasByVisibility("antigravity", quotas, visibility)).toHaveLength(2);
});
+193
View File
@@ -0,0 +1,193 @@
/**
* Qoder context-window tiers + routable model listing.
*
* The Qoder IDE lets a user pick 200K / 400K / 1M for a model; qodercli-style
* requests (what 9router sends) only carry the default max_input_tokens. These
* tests pin the escalation policy and the payload fields the IDE writes.
*/
import { describe, it, expect } from "vitest";
import {
parseTierTokenCount,
getQoderContextTiers,
estimateQoderPromptTokens,
resolveQoderContextTier,
applyQoderContextTier,
} from "../../open-sse/shared/qoder/contextTier.js";
import { routableQoderModels } from "../../open-sse/services/qoderModels.js";
// Shape mirrors the live /algo/api/v2/model/list entry for qmodel_38max.
const MODEL_CONFIG = {
key: "qmodel_38max",
display_name: "Qwen3.8-Max",
is_reasoning: true,
max_input_tokens: 180_000,
max_output_tokens: 32_768,
context_config: [
{ name: "200K", tokenCount: 200_000, isDefault: true },
{ name: "400K", tokenCount: 400_000, isDefault: false },
{ name: "1M", tokenCount: 1_000_000, isDefault: false },
],
};
function promptOfTokens(n) {
// ~4 ASCII chars per token
return { system: "", messages: [{ role: "user", content: "abcd".repeat(n) }], tools: [] };
}
describe("parseTierTokenCount", () => {
it("accepts numbers and K/M suffixed strings", () => {
expect(parseTierTokenCount(204800)).toBe(204800);
expect(parseTierTokenCount("200K")).toBe(200_000);
expect(parseTierTokenCount("1M")).toBe(1_000_000);
expect(parseTierTokenCount("1.5m")).toBe(1_500_000);
expect(parseTierTokenCount("131072")).toBe(131072);
});
it("returns 0 for garbage", () => {
expect(parseTierTokenCount(null)).toBe(0);
expect(parseTierTokenCount("big")).toBe(0);
expect(parseTierTokenCount(-5)).toBe(0);
});
});
describe("getQoderContextTiers", () => {
it("sorts tiers ascending and keeps the default flag", () => {
const tiers = getQoderContextTiers({
context_config: [
{ name: "1M", tokenCount: 1_000_000 },
{ name: "200K", tokenCount: 200_000, isDefault: true },
],
});
expect(tiers.map((t) => t.tokenCount)).toEqual([200_000, 1_000_000]);
expect(tiers[0].isDefault).toBe(true);
expect(tiers[1].isDefault).toBe(false);
});
it("understands camelCase / snake_case variants and derives names", () => {
const tiers = getQoderContextTiers({
contextConfig: [{ token_count: "400K", is_default: true }, { max_input_tokens: 1_000_000 }],
});
expect(tiers).toEqual([
{ name: "400K", tokenCount: 400_000, isDefault: true },
{ name: "1M", tokenCount: 1_000_000, isDefault: false },
]);
});
it("returns [] when the model has no tiers", () => {
expect(getQoderContextTiers({ max_input_tokens: 131072 })).toEqual([]);
expect(getQoderContextTiers(null)).toEqual([]);
});
});
describe("estimateQoderPromptTokens", () => {
it("counts CJK characters as ~1 token each instead of chars/4", () => {
const ascii = estimateQoderPromptTokens({ messages: [{ role: "user", content: "a".repeat(4000) }] });
const cjk = estimateQoderPromptTokens({ messages: [{ role: "user", content: "中".repeat(4000) }] });
expect(ascii).toBeLessThan(1_200);
expect(cjk).toBeGreaterThan(4_000);
});
});
describe("resolveQoderContextTier (auto)", () => {
it("leaves the payload untouched while the prompt fits the current max_input_tokens", () => {
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(50_000))).toBeNull();
});
it("escalates to the smallest tier that fits once the prompt outgrows the default", () => {
const choice = resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(250_000));
expect(choice).not.toBeNull();
expect(choice.tier.name).toBe("400K");
expect(choice.reason).toBe("auto:fits");
expect(choice.estimatedTokens).toBeGreaterThan(240_000);
});
it("falls back to the largest tier when nothing fits (upstream decides)", () => {
const choice = resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(1_200_000));
expect(choice.tier.name).toBe("1M");
expect(choice.reason).toBe("auto:largest");
});
it("applies headroom so a prompt just under the limit still escalates", () => {
// 170K estimated * 1.15 = 195.5K > 180K current → smallest tier above the current limit (200K)
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(170_000))?.tier.name).toBe("200K");
// 190K * 1.15 = 218.5K → 200K no longer fits → 400K
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(190_000))?.tier.name).toBe("400K");
});
it("returns null for models without context_config", () => {
expect(resolveQoderContextTier({ max_input_tokens: 131072 }, promptOfTokens(500_000))).toBeNull();
});
it("never escalates when the current limit is already the largest tier", () => {
const cfg = { ...MODEL_CONFIG, max_input_tokens: 1_000_000 };
expect(resolveQoderContextTier(cfg, promptOfTokens(1_500_000))).toBeNull();
});
});
describe("resolveQoderContextTier (forced via QODER_CONTEXT_TIER)", () => {
it("max picks the largest tier regardless of prompt size", () => {
const choice = resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(10), { preference: "max" });
expect(choice.tier.name).toBe("1M");
expect(choice.reason).toBe("forced:max");
});
it("default picks the isDefault tier", () => {
const choice = resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(10), { preference: "default" });
expect(choice.tier.name).toBe("200K");
});
it("a tier name or token count selects that tier", () => {
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(10), { preference: "400k" }).tier.tokenCount).toBe(400_000);
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(10), { preference: "1000000" }).tier.name).toBe("1M");
});
it("an unknown tier name falls back to auto", () => {
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(10), { preference: "9M" })).toBeNull();
expect(resolveQoderContextTier(MODEL_CONFIG, promptOfTokens(250_000), { preference: "9M" }).tier.name).toBe("400K");
});
});
describe("applyQoderContextTier", () => {
it("mirrors the tier into the three places the IDE writes", () => {
const payload = {
parameters: { max_tokens: 32_768 },
chat_context: { extra: { context: [], modelConfig: { key: "qmodel_38max" } } },
model_config: { ...MODEL_CONFIG },
};
applyQoderContextTier(payload, { name: "1M", tokenCount: 1_000_000 });
expect(payload.parameters).toEqual({ max_tokens: 32_768, context_length: 1_000_000 });
expect(payload.chat_context.extra.ideModelConfigOverride).toEqual({ max_input_tokens: 1_000_000 });
expect(payload.chat_context.extra.modelConfig).toEqual({ key: "qmodel_38max" });
expect(payload.model_config.max_input_tokens).toBe(1_000_000);
expect(payload.model_config.context_config).toHaveLength(3);
});
it("is a no-op without a tier", () => {
const payload = { parameters: { max_tokens: 1 } };
expect(applyQoderContextTier(payload, null)).toBe(payload);
expect(payload).toEqual({ parameters: { max_tokens: 1 } });
});
});
describe("routableQoderModels", () => {
it("lists visible models first, then hidden (enable:false) catalog keys", () => {
const catalog = {
models: [{ id: "qmodel_38max", name: "Qwen3.8-Max" }],
rawConfigs: new Map([
["qmodel_38max", { key: "qmodel_38max", enable: true }],
["qfmodel", { key: "qfmodel", enable: false, display_name: "Qwen Fast" }],
["dmodel", { key: "dmodel", enable: false }],
]),
};
expect(routableQoderModels(catalog)).toEqual([
{ id: "qmodel_38max", name: "Qwen3.8-Max", hidden: false },
{ id: "qfmodel", name: "Qwen Fast", hidden: true },
{ id: "dmodel", name: "dmodel", hidden: true },
]);
});
it("returns [] for a failed catalog fetch", () => {
expect(routableQoderModels(null)).toEqual([]);
});
});
+273 -1
View File
@@ -9,7 +9,7 @@
* - device flow URL construction
*/
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";
import crypto from "crypto";
import { qoderEncodeBody } from "../../src/lib/qoder/encoding.js";
@@ -22,6 +22,13 @@ import {
} from "../../src/lib/qoder/constants.js";
import { PROVIDER_MODELS } from "../../open-sse/config/providerModels.js";
import { __test__ as qoderExecutorInternals } from "../../open-sse/executors/qoder.js";
import { canonicalizeQoderUsage } from "../../open-sse/shared/qoder/sse.js";
import {
rewriteQoderMessageAttachments,
clearQoderUploadCache,
buildMultipartFile,
} from "../../open-sse/shared/qoder/attachments.js";
import { qoderInferenceBase } from "../../open-sse/shared/qoder/constants.js";
// Convenience aliases — tests were originally written against module-level
// helpers; the QoderService class wraps them so each test creates its own
@@ -367,6 +374,85 @@ describe("normalizeMessages", () => {
expect(result.messages).toEqual([]);
expect(result.systemText).toBe("");
});
it("preserves image_url blocks (http URL) instead of dropping them", () => {
const result = normalizeMessages([
{
role: "user",
content: [
{ type: "text", text: "describe" },
{ type: "image_url", image_url: { url: "https://example.com/a.png" } },
],
},
]);
const content = result.messages[0].content;
expect(Array.isArray(content)).toBe(true);
expect(content).toContainEqual({ type: "text", text: "describe" });
expect(content).toContainEqual({ type: "image_url", image_url: { url: "https://example.com/a.png" } });
});
it("preserves base64 data: URI images (no OSS upload needed)", () => {
const dataUri = "data:image/png;base64,iVBORw0KGgo=";
const result = normalizeMessages([
{
role: "user",
content: [
{ type: "image_url", image_url: { url: dataUri } },
{ type: "text", text: "what color?" },
],
},
]);
const content = result.messages[0].content;
expect(Array.isArray(content)).toBe(true);
expect(content[0]).toEqual({ type: "image_url", image_url: { url: dataUri } });
expect(content.some((b) => b.type === "text" && b.text === "what color?")).toBe(true);
});
it("converts claude-style base64 image blocks to image_url data URIs", () => {
const result = normalizeMessages([
{
role: "user",
content: [
{ type: "text", text: "see this" },
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "AAAA" } },
],
},
]);
const content = result.messages[0].content;
expect(content).toContainEqual({
type: "image_url",
image_url: { url: "data:image/jpeg;base64,AAAA" },
});
});
it("drops image blocks with no usable url but keeps the text", () => {
const result = normalizeMessages([
{
role: "user",
content: [
{ type: "text", text: "hi" },
{ type: "image_url", image_url: {} },
{ type: "image", source: { type: "base64" } },
],
},
]);
expect(result.messages[0].content).toBe("hi");
});
it("turns leftover file/document blocks into short stubs instead of dropping them", () => {
const result = normalizeMessages([
{
role: "user",
content: [
{ type: "text", text: "see" },
{ type: "file", file: { filename: "big.pdf", file_data: "data:application/pdf;base64,AAA" } },
],
},
]);
expect(result.messages[0].content).toContain("see");
expect(result.messages[0].content).toContain("big.pdf");
expect(result.messages[0].content).not.toContain("AAA");
});
});
describe("wrapQoderSSE", () => {
@@ -466,4 +552,190 @@ describe("wrapQoderSSE", () => {
const wrapped = await wrapQoderSSE(r, "qoder/auto");
expect(wrapped).toBe(r);
});
function envelope(body) {
return `data: ${JSON.stringify({ statusCodeValue: 200, body })}\n\n`;
}
function parseForwardedChunks(out) {
return out
.split("\n\n")
.map((block) => block.trim())
.filter((block) => block.startsWith("data:") && !block.includes("[DONE]"))
.map((block) => JSON.parse(block.slice("data:".length).trim()));
}
it("coalesces empty finish-in-delta + usage-only into one OpenAI usage chunk", async () => {
const content = JSON.stringify({
id: "chatcmpl-qoder-1",
created: 1700000000,
model: "auto",
choices: [{ index: 0, delta: { content: "hi" } }],
});
const finish = JSON.stringify({
id: "chatcmpl-qoder-1",
choices: [{ index: 0, delta: { content: "", finish_reason: "stop" } }],
});
const usage = JSON.stringify({
id: "chatcmpl-qoder-1",
choices: [],
usage: {
prompt_tokens: 100,
completion_tokens: 20,
total_tokens: 120,
prompt_tokens_details: { cached_tokens: 40 },
},
});
const wrapped = await wrapQoderSSE(
makeResponse([envelope(content) + envelope(finish) + envelope(usage) + envelope("[DONE]")]),
"qoder/auto",
);
const out = await drain(wrapped);
expect(out).toContain(`data: ${content}\n\n`);
const chunks = parseForwardedChunks(out);
const usageChunk = chunks.find((c) => c.usage);
expect(usageChunk).toBeDefined();
expect(usageChunk.choices[0].finish_reason).toBe("stop");
expect(usageChunk.usage.prompt_tokens).toBe(100);
expect(usageChunk.usage.completion_tokens).toBe(20);
expect(usageChunk.usage.prompt_tokens_details.cached_tokens).toBe(40);
expect(chunks.some((c) => Array.isArray(c.choices) && c.choices.length === 0)).toBe(false);
expect((out.match(/data: \[DONE\]/g) || []).length).toBe(1);
});
it("maps Qoder input_tokens aliases onto prompt_tokens in the coalesced usage chunk", async () => {
const finish = JSON.stringify({
choices: [{ index: 0, delta: { finish_reason: "stop" } }],
});
const usage = JSON.stringify({
choices: [],
usage: {
input_tokens: 80,
output_tokens: 10,
cache_read_input_tokens: 25,
},
});
const wrapped = await wrapQoderSSE(
makeResponse([envelope(finish) + envelope(usage)]),
"qoder/lite",
);
const chunks = parseForwardedChunks(await drain(wrapped));
const usageChunk = chunks.find((c) => c.usage);
expect(usageChunk.usage.prompt_tokens).toBe(80);
expect(usageChunk.usage.completion_tokens).toBe(10);
expect(usageChunk.usage.prompt_tokens_details.cached_tokens).toBe(25);
});
});
describe("canonicalizeQoderUsage", () => {
it("returns null for missing or empty usage", () => {
expect(canonicalizeQoderUsage(null)).toBeNull();
expect(canonicalizeQoderUsage({})).toBeNull();
});
it("copies prompt_tokens_details.cached_tokens through", () => {
const out = canonicalizeQoderUsage({
prompt_tokens: 50,
completion_tokens: 5,
prompt_tokens_details: { cached_tokens: 12 },
});
expect(out.prompt_tokens).toBe(50);
expect(out.cached_tokens).toBe(12);
expect(out.prompt_tokens_details.cached_tokens).toBe(12);
expect(out.total_tokens).toBe(55);
});
});
describe("qoderInferenceBase", () => {
it("sends job tokens to api2 and device tokens to api3", () => {
expect(qoderInferenceBase({ accessToken: "jt-abc" })).toContain("api2.qoder.sh");
expect(qoderInferenceBase({ accessToken: "dt-abc" })).toContain("api3.qoder.sh");
});
});
describe("rewriteQoderMessageAttachments", () => {
beforeEach(() => clearQoderUploadCache());
it("uploads data-URI images and keeps only the OSS URL in the message", async () => {
const messages = [{
role: "user",
content: [
{ type: "text", text: "see this" },
{ type: "image_url", image_url: { url: "data:image/png;base64,AAAA" } },
],
}];
const stats = await rewriteQoderMessageAttachments(messages, {
uploadFn: async ({ buffer, mediaType }) => {
expect(Buffer.isBuffer(buffer)).toBe(true);
expect(mediaType).toBe("image/png");
return "https://cdn.qoder.example/img.png";
},
});
expect(messages[0].content).toEqual([
{ type: "text", text: "see this" },
{ type: "image_url", image_url: { url: "https://cdn.qoder.example/img.png" } },
]);
expect(JSON.stringify(messages)).not.toContain("AAAA");
expect(stats.imageUrls).toEqual(["https://cdn.qoder.example/img.png"]);
});
it("does not re-upload already-hosted http(s) image URLs", async () => {
const messages = [{
role: "user",
content: [{ type: "image_url", image_url: { url: "https://example.com/a.png" } }],
}];
await rewriteQoderMessageAttachments(messages, {
uploadFn: async () => {
throw new Error("should not upload remote URLs");
},
});
expect(messages[0].content[0].image_url.url).toBe("https://example.com/a.png");
});
it("stubs non-image file blocks instead of inlining bytes", async () => {
const pdfB64 = "A".repeat(200);
const messages = [{
role: "user",
content: [
{ type: "text", text: "read this" },
{ type: "file", file: { filename: "big.pdf", file_data: `data:application/pdf;base64,${pdfB64}` } },
],
}];
await rewriteQoderMessageAttachments(messages, {
uploadFn: async () => {
throw new Error("should not upload PDFs as images");
},
});
const wire = JSON.stringify(messages);
expect(wire).not.toContain(pdfB64);
expect(wire).toContain("[file omitted: big.pdf");
});
it("stubs oversized images when OSS upload fails instead of keeping a huge data URI", async () => {
const big = "A".repeat(700_000);
const messages = [{
role: "user",
content: [{ type: "image_url", image_url: { url: `data:image/png;base64,${big}` } }],
}];
await rewriteQoderMessageAttachments(messages, {
uploadFn: async () => {
throw new Error("upstream 413");
},
});
const wire = JSON.stringify(messages);
expect(wire).not.toContain(big);
expect(wire).toContain("[file omitted:");
expect(Buffer.byteLength(wire, "utf8")).toBeLessThan(4096);
});
it("buildMultipartFile uses the file field name qodercli sends", () => {
const { boundary, body } = buildMultipartFile(Buffer.from("hi"), {
fileName: "image.png",
mediaType: "image/png",
});
const text = body.toString("latin1");
expect(text).toContain(`name="file"`);
expect(text).toContain("filename=\"image.png\"");
expect(text).toContain(`--${boundary}`);
});
});
+71 -1
View File
@@ -1,7 +1,9 @@
import { describe, expect, it } from "vitest";
import { createDisconnectAwareStream } from "../../open-sse/utils/streamHandler.js";
import { createDisconnectAwareStream, pipeWithDisconnect, createStreamController } from "../../open-sse/utils/streamHandler.js";
import { buildAbortedResponsesTerminalBytes } from "../../open-sse/utils/responsesStreamHelpers.js";
import { buildStreamErrorBytes } from "../../open-sse/utils/streamHelpers.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
// Minimal stream controller stub
function makeController() {
@@ -70,3 +72,71 @@ describe("Responses abort terminal synthesis", () => {
expect(text).not.toContain("[DONE]");
});
});
// A stream that aborts after HTTP 200 cannot change status, so the failure must
// travel in-band: structured error frame first, then [DONE]. openai-python raises
// APIError on any `data:` payload carrying an `error` key (checked before [DONE]);
// Anthropic clients need `event: error`. Never a fabricated finish_reason.
describe("buildStreamErrorBytes", () => {
const jsonOf = (sse) => JSON.parse(sse.match(/\{.*\}/s)[0]);
const textOf = (bytes) => new TextDecoder().decode(bytes);
// onAbortTerminal callbacks are enqueued verbatim, so a string here is a
// silent no-op at runtime (createDisconnectAwareStream swallows the throw).
it("returns bytes, not a string", () => {
expect(buildStreamErrorBytes(504, "x", FORMATS.OPENAI)).toBeInstanceOf(Uint8Array);
});
it("emits error frame then [DONE] for OpenAI clients", () => {
const out = textOf(buildStreamErrorBytes(504, "stream stall timeout", FORMATS.OPENAI));
expect(out).toContain('data: {"error"');
expect(out.indexOf("data: [DONE]")).toBeGreaterThan(out.indexOf('data: {"error"'));
expect(jsonOf(out).error).toEqual({
message: "stream stall timeout",
type: "server_error",
code: "gateway_timeout",
});
});
it("emits event: error (no [DONE]) for Claude clients", () => {
const out = textOf(buildStreamErrorBytes(504, "stream stall timeout", FORMATS.CLAUDE));
expect(out).toContain("event: error\n");
expect(out).not.toContain("[DONE]");
expect(jsonOf(out)).toMatchObject({ type: "error", error: { message: "stream stall timeout" } });
});
});
// The wiring, not just the frame builder: the watchdog must hand its reason to
// onAbortTerminal and the bytes must reach a real consumer.
describe("stall abort through pipeWithDisconnect", () => {
it("delivers the error frame and closes the stream", async () => {
// Real controller: the stub above never fires its signal, and the abort
// must reach the upstream body for the pipe to end.
const ctrl = createStreamController({ provider: "ollama", model: "test" });
// Emits one chunk then goes silent; errors on abort like a real fetch body.
const upstream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("data: hi\n\n"));
ctrl.signal.addEventListener("abort", () => controller.error(new Error("aborted")), { once: true });
},
});
let seen = null;
const out = pipeWithDisconnect(
{ body: upstream },
new TransformStream(),
ctrl,
(message) => { seen = message; return buildStreamErrorBytes(504, message, FORMATS.OPENAI); },
50
);
const text = await readAll(out);
expect(seen).toBe("stream stall timeout");
expect(text).toContain('"stream stall timeout"');
expect(text).toContain("data: [DONE]");
});
});
@@ -0,0 +1,165 @@
// Parallel function_calls from a Responses upstream must stay on separate
// chat tool_calls indices. Regression: response/openai-responses.js attributed
// every arguments delta to the positional toolCallIndex (advanced only on
// output_item.done), so all-added-then-deltas ordering concatenated N JSON
// payloads into index 0 and clients failed with InputValidationError.
import { describe, expect, it } from "vitest";
import "../translator/registerAll.js";
import { openaiResponsesToOpenAIResponse } from "../../open-sse/translator/response/openai-responses.js";
import { clampResponsesCallId, coerceResponsesOutput, MAX_RESPONSES_CALL_ID_LEN } from "../../open-sse/translator/formats/responsesApi.js";
import { initState, translateResponse } from "../../open-sse/translator/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
const added = (id, call_id, name, type = "function_call") => ({
type: "response.output_item.added",
item: { id, type, call_id, name, arguments: "" },
});
const delta = (item_id, text) => ({
type: "response.function_call_arguments.delta",
item_id,
delta: text,
});
const done = (id, call_id, name) => ({
type: "response.output_item.done",
item: { id, type: "function_call", call_id, name },
});
// Reassemble translated chunks the way an OpenAI client accumulator does.
function accumulate(calls, chunks) {
for (const chunk of chunks) {
if (!chunk) continue;
for (const tc of chunk.choices?.[0]?.delta?.tool_calls || []) {
const slot = (calls[tc.index] ??= { id: null, name: "", args: "" });
if (tc.id) slot.id = tc.id;
if (tc.function?.name) slot.name = tc.function.name;
if (tc.function?.arguments) slot.args += tc.function.arguments;
}
}
return calls;
}
function runStream(events) {
const state = {};
const chunks = [];
for (const ev of events) {
const out = openaiResponsesToOpenAIResponse(ev, state);
if (out) chunks.push(out);
}
const flush = openaiResponsesToOpenAIResponse(null, state);
if (flush) chunks.push(flush);
return { state, chunks };
}
const PAYLOADS = [
'{"file_path":"/docs/PRODUCT.md"}',
'{"file_path":"/docs/ROADMAP.md"}',
'{"file_path":"/docs/openapi.custom.yaml"}',
'{"file_path":"/docs/.gitignore"}',
];
function hostileOrdering() {
const events = PAYLOADS.map((_, i) => added(`fc_${i}`, `call_${i}`, "read_file"));
// Interleaved deltas AFTER all addeds — the ordering that used to merge all
// four payloads into index 0.
PAYLOADS.forEach((p, i) => events.push(delta(`fc_${i}`, p.slice(0, 20)), delta(`fc_${i}`, p.slice(20))));
PAYLOADS.forEach((_, i) => events.push(done(`fc_${i}`, `call_${i}`, "read_file")));
return events;
}
describe("responses parallel tool calls keep their own index", () => {
it("all-added-then-deltas ordering yields 4 separately parseable calls", () => {
const { chunks } = runStream(hostileOrdering());
const calls = accumulate({}, chunks);
expect(Object.keys(calls)).toHaveLength(4);
PAYLOADS.forEach((p, i) => {
expect(calls[i].id).toBe(`call_${i}`);
expect(calls[i].name).toBe("read_file");
expect(JSON.parse(calls[i].args)).toEqual(JSON.parse(p));
});
});
it("sequential ordering still yields indices 0,1 in order", () => {
const events = [
added("fc_0", "call_0", "read_file"),
delta("fc_0", PAYLOADS[0]),
done("fc_0", "call_0", "read_file"),
added("fc_1", "call_1", "read_file"),
delta("fc_1", PAYLOADS[1]),
done("fc_1", "call_1", "read_file"),
];
const { chunks } = runStream(events);
const calls = accumulate({}, chunks);
expect(Object.keys(calls)).toEqual(["0", "1"]);
expect(JSON.parse(calls[0].args)).toEqual(JSON.parse(PAYLOADS[0]));
expect(JSON.parse(calls[1].args)).toEqual(JSON.parse(PAYLOADS[1]));
});
it("done carrying full arguments (no deltas) emits them once", () => {
const state = {};
const out1 = openaiResponsesToOpenAIResponse(added("fc_9", "call_9", "read_file"), state);
const out2 = openaiResponsesToOpenAIResponse({
type: "response.output_item.done",
item: { id: "fc_9", type: "function_call", call_id: "call_9", name: "read_file", arguments: PAYLOADS[0] },
}, state);
const calls = accumulate({}, [out1, out2]);
expect(JSON.parse(calls[0].args)).toEqual(JSON.parse(PAYLOADS[0]));
});
it("deltas without item_id fall back to the most recent call (legacy behavior)", () => {
const events = [
added("fc_0", "call_0", "read_file"),
{ type: "response.function_call_arguments.delta", delta: PAYLOADS[0] },
done("fc_0", "call_0", "read_file"),
];
const { chunks } = runStream(events);
const calls = accumulate({}, chunks);
expect(JSON.parse(calls[0].args)).toEqual(JSON.parse(PAYLOADS[0]));
});
});
describe("responses → claude end-to-end keeps parallel tool_use blocks separate", () => {
it("four read_file calls arrive as four parseable tool_use blocks", () => {
const state = initState(FORMATS.CLAUDE);
const out = [];
for (const ev of hostileOrdering()) {
for (const r of translateResponse(FORMATS.OPENAI_RESPONSES, FORMATS.CLAUDE, ev, state)) out.push(r);
}
for (const r of translateResponse(FORMATS.OPENAI_RESPONSES, FORMATS.CLAUDE, null, state)) out.push(r);
const starts = out.filter((r) => r?.type === "content_block_start" && r?.content_block?.type === "tool_use");
expect(starts).toHaveLength(4);
const partials = out.filter((r) => r?.delta?.type === "input_json_delta");
expect(partials).toHaveLength(4);
const bodies = partials.map((r) => JSON.parse(r.delta.partial_json).file_path).sort();
expect(bodies).toEqual([
"/docs/.gitignore",
"/docs/PRODUCT.md",
"/docs/ROADMAP.md",
"/docs/openapi.custom.yaml",
]);
});
});
describe("fallback call_ids stay unique within a batch", () => {
it("same-millisecond fallbacks never collide", () => {
const ids = new Set(Array.from({ length: 50 }, () => clampResponsesCallId(undefined)));
expect(ids.size).toBe(50);
for (const id of ids) {
expect(id.startsWith("call_")).toBe(true);
expect(id.length).toBeLessThanOrEqual(MAX_RESPONSES_CALL_ID_LEN);
}
expect(new Set([clampResponsesCallId(""), clampResponsesCallId(null)]).size).toBe(2);
});
});
describe("output coercion stays fail-soft on unstringifiable values", () => {
it("never throws on BigInt/circular array elements", () => {
const circular = {};
circular.self = circular;
const input = [1n, circular, { text: "ok" }];
expect(() => coerceResponsesOutput(input)).not.toThrow();
const out = coerceResponsesOutput(input);
expect(typeof out).toBe("string");
expect(out).toContain("ok");
});
});
+59 -77
View File
@@ -261,138 +261,121 @@ describe("system-inject gemini", () => {
});
describe("system-inject kiro", () => {
it("updates systemPrompt and mirrored prefix of first history user preserving tail", () => {
const oldPrompt = "OLD_SYS";
// The kiro.dev gateway rejects any body carrying a top-level `systemPrompt`
// with 400 REQUEST_BODY_INVALID, so the prompt goes into the user turn only.
it("appends to first history user, leaves systemPrompt untouched", () => {
const timeCtx = "[Context: Current time is 2026-01-01T00:00:00.000Z]";
const tail = "user tail content";
const historyUserContent = `${oldPrompt}${SEP}${timeCtx}${SEP}${tail}`;
const historyUserContent = `${timeCtx}${SEP}${tail}`;
const body = {
systemPrompt: oldPrompt,
conversationState: {
history: [{ userInputMessage: { content: historyUserContent, modelId: "m" } }, { assistantResponseMessage: { content: "..." } }],
currentMessage: { userInputMessage: { content: "current " + tail, modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
const next = `${oldPrompt}${SEP}${P1}`;
expect(body.systemPrompt).toBe(next);
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${next}${SEP}${timeCtx}${SEP}${tail}`);
expect(body.systemPrompt).toBeUndefined();
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${historyUserContent}${SEP}${P1}`);
// currentMessage must stay untouched
expect(body.conversationState.currentMessage.userInputMessage.content).toBe("current " + tail);
});
it("when no history user, updates currentMessage instead", () => {
const oldPrompt = "OLD";
const body = {
systemPrompt: oldPrompt,
conversationState: {
history: [],
currentMessage: { userInputMessage: { content: `${oldPrompt}${SEP}tail`, modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}`);
expect(body.conversationState.currentMessage.userInputMessage.content).toBe(`${oldPrompt}${SEP}${P1}${SEP}tail`);
});
it("empty old prompt prepends to chosen user content", () => {
const body = {
systemPrompt: "",
conversationState: {
history: [{ userInputMessage: { content: "tail hello", modelId: "m" } }],
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(P1);
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${P1}${SEP}tail hello`);
});
it("if old prompt not mirrored at head, do not alter user content", () => {
it("never writes a top-level systemPrompt, even if one is already present", () => {
const body = {
systemPrompt: "OLD",
conversationState: {
history: [{ userInputMessage: { content: "different head content", modelId: "m" } }],
history: [{ userInputMessage: { content: "tail", modelId: "m" } }],
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe("OLD");
expect(body.conversationState.history[0].userInputMessage.content).toBe(`tail${SEP}${P1}`);
});
it("when no history user, updates currentMessage instead", () => {
const body = {
conversationState: {
history: [],
currentMessage: { userInputMessage: { content: "tail", modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBeUndefined();
expect(body.conversationState.currentMessage.userInputMessage.content).toBe(`tail${SEP}${P1}`);
});
it("empty user content becomes the prompt itself", () => {
const body = {
conversationState: {
history: [{ userInputMessage: { content: "", modelId: "m" } }],
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(`OLD${SEP}${P1}`);
expect(body.conversationState.history[0].userInputMessage.content).toBe("different head content");
expect(body.conversationState.history[0].userInputMessage.content).toBe(P1);
expect(body.conversationState.currentMessage.userInputMessage.content).toBe("cur");
});
it("exact retry idempotency for kiro", () => {
const oldPrompt = "OLD";
const body = {
systemPrompt: oldPrompt,
conversationState: {
history: [{ userInputMessage: { content: `${oldPrompt}${SEP}tail`, modelId: "m" } }],
history: [{ userInputMessage: { content: "tail", modelId: "m" } }],
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
const after1 = JSON.parse(JSON.stringify(body));
const after1 = body.conversationState.history[0].userInputMessage.content;
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(after1.systemPrompt);
expect(body.conversationState.history[0].userInputMessage.content).toBe(after1.conversationState.history[0].userInputMessage.content);
// different prompt both apply
expect(body.conversationState.history[0].userInputMessage.content).toBe(after1);
// different prompt both apply, in injection order
injectSystemPrompt(body, FORMATS.KIRO, P2);
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}${SEP}${P2}`);
expect(body.conversationState.history[0].userInputMessage.content).toBe(`tail${SEP}${P1}${SEP}${P2}`);
});
it("preserves non-enumerable _kiroUpstreamModel", () => {
const body = {
systemPrompt: "OLD",
conversationState: { history: [{ userInputMessage: { content: "OLD" + SEP + "tail", modelId: "m" } }], currentMessage: { userInputMessage: { content: "OLD" + SEP + "tail2", modelId: "m" } } },
conversationState: { history: [{ userInputMessage: { content: "tail", modelId: "m" } }], currentMessage: { userInputMessage: { content: "tail2", modelId: "m" } } },
};
Object.defineProperty(body, "_kiroUpstreamModel", { value: "m", enumerable: false });
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body._kiroUpstreamModel).toBe("m");
expect(Object.getOwnPropertyDescriptor(body, "_kiroUpstreamModel").enumerable).toBe(false);
});
it("frozen user message fails open without throwing or half-writing", () => {
const body = {
conversationState: {
history: [{ userInputMessage: Object.freeze({ content: "tail", modelId: "m" }) }],
},
};
expect(() => injectSystemPrompt(body, FORMATS.KIRO, P1)).not.toThrow();
expect(body.systemPrompt).toBeUndefined();
expect(body.conversationState.history[0].userInputMessage.content).toBe("tail");
});
});
describe("system-inject regression fixes", () => {
it("kiro partial mutation converges on retry after transient content write failure", () => {
const oldPrompt = "OLD";
let failNextWrite = true;
const um = { content: `${oldPrompt}${SEP}tail`, modelId: "m" };
const um = { content: "tail", modelId: "m" };
const proxiedUm = new Proxy(um, {
set(t, p, v) {
if (p === "content" && failNextWrite) { failNextWrite = false; throw new Error("transient"); }
t[p] = v; return true;
},
});
const body = {
systemPrompt: oldPrompt,
conversationState: {
history: [{ userInputMessage: proxiedUm }],
},
};
const body = { conversationState: { history: [{ userInputMessage: proxiedUm }] } };
injectSystemPrompt(body, FORMATS.KIRO, P1);
// first pass rolled back atomically — nothing half-applied
expect(body.systemPrompt).toBe(oldPrompt);
expect(um.content).toBe(`${oldPrompt}${SEP}tail`);
// nothing half-applied
expect(um.content).toBe("tail");
expect(body.systemPrompt).toBeUndefined();
// retry converges
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}`);
expect(um.content).toBe(`${oldPrompt}${SEP}${P1}${SEP}tail`);
});
it("kiro rolls back systemPrompt when user content write fails (atomicity)", () => {
const oldPrompt = "OLD";
const body = {
systemPrompt: oldPrompt,
conversationState: {
history: [{ userInputMessage: Object.freeze({ content: `${oldPrompt}${SEP}tail`, modelId: "m" }) }],
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.systemPrompt).toBe(oldPrompt);
expect(um.content).toBe(`tail${SEP}${P1}`);
});
it("kiro shape gate: stray conversationState without history/currentMessage does not hijack chat body", () => {
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }], systemPrompt: "", conversationState: {} };
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }], conversationState: {} };
injectSystemPrompt(body, FORMATS.OPENAI, P1);
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
});
@@ -409,15 +392,14 @@ describe("system-inject regression fixes", () => {
expect(body.instructions).toBe(`You are RULE follower${SEP}RULE`);
});
it("kiro empty-old prepend fires when prompt appears mid-tail only", () => {
it("substring occurrence does not suppress kiro injection", () => {
const body = {
systemPrompt: "",
conversationState: {
history: [{ userInputMessage: { content: `some ${P1} here`, modelId: "m" } }],
},
};
injectSystemPrompt(body, FORMATS.KIRO, P1);
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${P1}${SEP}some ${P1} here`);
expect(body.conversationState.history[0].userInputMessage.content).toBe(`some ${P1} here${SEP}${P1}`);
});
});
+1 -1
View File
@@ -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", "zed",
"deepseek", "opencode-go", "zed", "commandcode",
];
describe("usage dispatch", () => {
+296
View File
@@ -0,0 +1,296 @@
/**
* Unit tests for the OpenRouter + Vertex (Veo) video adapters.
*
* Covers:
* - registry wiring (videoConfig, video serviceKind, video-kind models)
* - OpenRouter: POST to the collection root, GET poll, verbatim passthrough
* - Vertex: predictLongRunning body translation, fetchPredictOperation polling,
* operation-name round-trip through the job id, response mapping
* - xAI default path is unchanged by the adapter hook
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("open-sse/services/tokenRefresh.js", async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, refreshTokenByProvider: vi.fn(), refreshVertexToken: vi.fn() };
});
import { handleVideoProxyCore, getVideoConfig } from "open-sse/handlers/videoCore.js";
import { refreshVertexToken } from "open-sse/services/tokenRefresh.js";
import { PROVIDER_MEDIA, PROVIDER_MODELS } from "open-sse/providers/index.js";
const originalFetch = global.fetch;
const jsonResponse = (body, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
// Vertex operation names are resource paths; the adapter base64url-encodes them.
const OPERATION_NAME =
"projects/proj-1/locations/us-central1/publishers/google/models/veo-3.1-generate-preview/operations/op-abc";
const JOB_ID = Buffer.from(OPERATION_NAME, "utf8").toString("base64url");
describe("registry wiring", () => {
it("exposes videoConfig + video serviceKind for openrouter and vertex", () => {
expect(getVideoConfig("openrouter").baseUrl).toBe("https://openrouter.ai/api/v1/videos");
expect(getVideoConfig("vertex").baseUrl).toBe("https://aiplatform.googleapis.com");
expect(PROVIDER_MEDIA.openrouter.serviceKinds).toContain("video");
expect(PROVIDER_MEDIA.vertex.serviceKinds).toContain("video");
});
it("registers video-kind models on both providers", () => {
const or = PROVIDER_MODELS.openrouter.find((m) => m.id === "google/veo-3.1");
const vx = PROVIDER_MODELS.vertex.find((m) => m.id === "veo-3.1-generate-preview");
expect(or?.kind).toBe("video");
expect(vx?.kind).toBe("video");
});
});
describe("openrouter video adapter", () => {
beforeEach(() => { global.fetch = vi.fn(); });
afterEach(() => { global.fetch = originalFetch; });
it("POSTs creation to the collection root (no /generations suffix)", async () => {
global.fetch.mockResolvedValueOnce(jsonResponse({ id: "job-1", status: "pending" }));
const raw = '{"model":"google/veo-3.1","prompt":"a paper boat"}';
const result = await handleVideoProxyCore({
provider: "openrouter",
action: "generations",
rawBody: raw,
contentType: "application/json",
credentials: { apiKey: "sk-or-key" },
});
expect(result.success).toBe(true);
const [url, init] = global.fetch.mock.calls[0];
expect(url).toBe("https://openrouter.ai/api/v1/videos");
expect(init.method).toBe("POST");
expect(init.body).toBe(raw); // verbatim
expect(init.headers.Authorization).toBe("Bearer sk-or-key");
expect(init.headers["HTTP-Referer"]).toBe("https://endpoint-proxy.local");
expect(await result.response.json()).toEqual({ id: "job-1", status: "pending" });
});
it("polls GET /videos/{id} and passes the payload through verbatim", async () => {
const payload = { id: "job-1", status: "completed", unsigned_urls: ["https://cdn/v.mp4"] };
global.fetch.mockResolvedValueOnce(jsonResponse(payload));
const result = await handleVideoProxyCore({
provider: "openrouter",
requestId: "job-1",
credentials: { apiKey: "sk-or-key" },
});
const [url, init] = global.fetch.mock.calls[0];
expect(url).toBe("https://openrouter.ai/api/v1/videos/job-1");
expect(init.method).toBe("GET");
expect(await result.response.json()).toEqual(payload);
});
it("rejects unsupported actions before any upstream call (no billable job)", async () => {
const result = await handleVideoProxyCore({
provider: "openrouter",
action: "extensions",
rawBody: "{}",
contentType: "application/json",
credentials: { apiKey: "sk-or-key" },
});
expect(result.success).toBe(false);
expect(result.status).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe("vertex (veo) video adapter", () => {
beforeEach(() => {
global.fetch = vi.fn();
refreshVertexToken.mockReset();
});
afterEach(() => { global.fetch = originalFetch; });
const saJson = JSON.stringify({
type: "service_account",
client_email: "sa@proj-1.iam.gserviceaccount.com",
private_key: "-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----\n",
project_id: "proj-1",
});
it("translates the create body to predictLongRunning and returns a poll-able job id", async () => {
refreshVertexToken.mockResolvedValueOnce({ accessToken: "vertex-tok" });
global.fetch.mockResolvedValueOnce(jsonResponse({ name: OPERATION_NAME }));
const result = await handleVideoProxyCore({
provider: "vertex",
action: "generations",
rawBody: JSON.stringify({
model: "veo-3.1-generate-preview",
prompt: "a neon city",
duration: 8,
aspect_ratio: "16:9",
resolution: "720p",
n: 1,
}),
contentType: "application/json",
credentials: { apiKey: saJson },
});
expect(result.success).toBe(true);
const [url, init] = global.fetch.mock.calls[0];
expect(url).toBe(
"https://aiplatform.googleapis.com/v1/projects/proj-1/locations/us-central1/publishers/google/models/veo-3.1-generate-preview:predictLongRunning"
);
expect(init.headers.Authorization).toBe("Bearer vertex-tok");
expect(JSON.parse(init.body)).toEqual({
instances: [{ prompt: "a neon city" }],
parameters: { sampleCount: 1, durationSeconds: 8, aspectRatio: "16:9", resolution: "720p" },
});
// Response is mapped onto the async-job shape clients already poll.
expect(await result.response.json()).toEqual({
id: JOB_ID,
request_id: JOB_ID,
status: "pending",
});
});
it("maps a data-URL image onto the Vertex image instance", async () => {
refreshVertexToken.mockResolvedValueOnce({ accessToken: "vertex-tok" });
global.fetch.mockResolvedValueOnce(jsonResponse({ name: OPERATION_NAME }));
await handleVideoProxyCore({
provider: "vertex",
action: "generations",
rawBody: JSON.stringify({
model: "veo-3.1-generate-preview",
prompt: "animate this",
image: "data:image/png;base64,AAAB",
}),
contentType: "application/json",
credentials: { apiKey: saJson },
});
expect(JSON.parse(global.fetch.mock.calls[0][1].body).instances[0].image).toEqual({
bytesBase64Encoded: "AAAB",
mimeType: "image/png",
});
});
it("polls via fetchPredictOperation and maps a completed operation", async () => {
refreshVertexToken.mockResolvedValueOnce({ accessToken: "vertex-tok" });
global.fetch.mockResolvedValueOnce(
jsonResponse({
name: OPERATION_NAME,
done: true,
response: { videos: [{ gcsUri: "gs://bucket/v.mp4", mimeType: "video/mp4" }] },
})
);
const result = await handleVideoProxyCore({
provider: "vertex",
requestId: JOB_ID,
credentials: { apiKey: saJson },
});
const [url, init] = global.fetch.mock.calls[0];
expect(url).toBe(
"https://aiplatform.googleapis.com/v1/projects/proj-1/locations/us-central1/publishers/google/models/veo-3.1-generate-preview:fetchPredictOperation"
);
expect(init.method).toBe("POST"); // Vertex polls with POST, not GET
expect(JSON.parse(init.body)).toEqual({ operationName: OPERATION_NAME });
expect(await result.response.json()).toEqual({
id: JOB_ID,
request_id: JOB_ID,
status: "completed",
video: { url: "gs://bucket/v.mp4", b64_json: null, mime_type: "video/mp4" },
videos: [{ url: "gs://bucket/v.mp4", b64_json: null, mime_type: "video/mp4" }],
});
});
it("maps a failed operation to status failed", async () => {
refreshVertexToken.mockResolvedValueOnce({ accessToken: "vertex-tok" });
global.fetch.mockResolvedValueOnce(
jsonResponse({ name: OPERATION_NAME, done: true, error: { code: 3, message: "bad prompt" } })
);
const result = await handleVideoProxyCore({
provider: "vertex",
requestId: JOB_ID,
credentials: { apiKey: saJson },
});
const body = await result.response.json();
expect(body.status).toBe("failed");
expect(body.error.message).toBe("bad prompt");
});
it("rejects missing project id and raw API keys before any upstream call", async () => {
const noProject = await handleVideoProxyCore({
provider: "vertex",
action: "generations",
rawBody: JSON.stringify({ model: "veo-3.1-generate-preview", prompt: "x" }),
contentType: "application/json",
credentials: { apiKey: "AIzaRawKey" },
});
expect(noProject.success).toBe(false);
expect(noProject.status).toBe(400);
const noToken = await handleVideoProxyCore({
provider: "vertex",
action: "generations",
rawBody: JSON.stringify({ model: "veo-3.1-generate-preview", prompt: "x" }),
contentType: "application/json",
credentials: { apiKey: "AIzaRawKey", providerSpecificData: { projectId: "proj-1" } },
});
expect(noToken.success).toBe(false);
expect(noToken.status).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
});
it("rejects an invalid job id without calling upstream", async () => {
refreshVertexToken.mockResolvedValue({ accessToken: "vertex-tok" });
const result = await handleVideoProxyCore({
provider: "vertex",
requestId: Buffer.from("not-an-operation", "utf8").toString("base64url"),
credentials: { apiKey: saJson },
});
expect(result.success).toBe(false);
expect(result.status).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
});
// A base64url id decodes to arbitrary bytes, so a crafted one used to splice a
// path traversal into the fetch URL while the Authorization header stayed on.
it("rejects job ids that decode outside the projects/…/operations/ shape", async () => {
refreshVertexToken.mockResolvedValue({ accessToken: "vertex-tok" });
const jid = (s) => Buffer.from(s, "utf8").toString("base64url");
for (const id of [
jid("../../evil"),
jid("projects/p/locations/l/publishers/google/models/m/operations/../../x"),
jid("../../evil/operations/op"),
"!!!not-base64!!!",
`${JOB_ID}=`,
`${JOB_ID}\n`,
]) {
const result = await handleVideoProxyCore({ provider: "vertex", requestId: id, credentials: { apiKey: saJson } });
expect(result.status).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
}
});
it("rejects a model id carrying path separators", async () => {
refreshVertexToken.mockResolvedValue({ accessToken: "vertex-tok" });
const result = await handleVideoProxyCore({
provider: "vertex",
action: "generations",
rawBody: JSON.stringify({ model: "../../evil", prompt: "x" }),
contentType: "application/json",
credentials: { apiKey: saJson },
});
expect(result.status).toBe(400);
expect(global.fetch).not.toHaveBeenCalled();
});
});
+1
View File
@@ -29,6 +29,7 @@ vi.mock("@/sse/services/auth.js", () => authMocks);
vi.mock("@/sse/services/tokenRefresh.js", () => tokenMocks);
vi.mock("@/lib/localDb", () => ({
getSettings: vi.fn(async () => ({ requireApiKey: false })),
getProviderConnectionById: vi.fn(async () => ({ id: "conn-5", provider: "xai" })),
getComboByName: vi.fn(async () => null),
getModelAliases: vi.fn(async () => ({})),
getProviderNodes: vi.fn(async () => []),
Loaded 50 of 208 files, more files were not shown because too many files have changed in this diff. Show more