Merge origin/master (v0.5.75) into gitea/new_feature

Resolve conflicts:
- package.json / cli/package.json: take 0.5.75
- .gitignore: union both sides (upstream 9router-*/temp files + local state dirs)
- CHANGELOG.md: keep both blocks, v0.5.75 above v0.5.70
- nonStreamingHandler.js: merge imports (unwrapClineEnvelope +
  tokensForDetail/shouldPersistRequestDetail); drop dead appendRequestLog
- providers/[id]/page.js: union useState blocks (compatible-model states
  + importingClineModels)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
2026-09-17 14:14:43 +07:00
113 changed files with 6634 additions and 482 deletions

View File

@@ -192,9 +192,10 @@
"baseUrl": "https://chatgpt.com/backend-api/codex/responses",
"format": "openai-responses",
"forceStream": true,
"cliVersion": "0.154.0",
"headers": {
"originator": "codex_cli_rs",
"User-Agent": "codex_cli_rs/0.136.0"
"User-Agent": "codex_cli_rs/0.154.0"
},
"usage": {
"url": "https://chatgpt.com/backend-api/wham/usage",

View File

@@ -0,0 +1,33 @@
// Regression for #3905: defaultClaudeToolType() (type:"custom") must only run for
// gateways that declare the requireClaudeToolType quirk (MiniMax). Claude-format
// endpoints that only accept the legacy typeless tool shape — e.g. DeepSeek's
// Anthropic-compatible endpoint, which answers HTTP 400 "unknown variant `custom`" —
// must never receive tools[].type = "custom".
import { describe, it, expect } from "vitest";
import { PROVIDERS } from "../../open-sse/providers/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
import { shouldDefaultClaudeToolType } from "../../open-sse/translator/concerns/toolCall.js";
const tools = [{ name: "get_weather", description: "weather", input_schema: { type: "object" } }];
describe("Claude tool `type` defaulting is provider-scoped (#3905)", () => {
it("runs only for providers declaring requireClaudeToolType", () => {
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(true);
expect(shouldDefaultClaudeToolType("minimax-cn", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(true);
// Endpoints accepting only the legacy typeless shape must NOT get type:"custom".
expect(shouldDefaultClaudeToolType("deepseek", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(false);
expect(shouldDefaultClaudeToolType("claude", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(false);
});
it("never applies outside Claude-format requests or without tools", () => {
expect(shouldDefaultClaudeToolType("minimax", FORMATS.OPENAI, tools, PROVIDERS)).toBe(false);
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, undefined, PROVIDERS)).toBe(false);
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, null, PROVIDERS)).toBe(false);
});
it("declares the quirk only on the MiniMax providers (registry tripwire)", () => {
expect(PROVIDERS.minimax?.quirks?.requireClaudeToolType).toBe(true);
expect(PROVIDERS["minimax-cn"]?.quirks?.requireClaudeToolType).toBe(true);
expect(PROVIDERS.deepseek?.quirks?.requireClaudeToolType).toBeUndefined();
});
});

View File

@@ -26,9 +26,8 @@ describe("Claude → Kiro (direct route)", () => {
expect(first.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
expect(second.conversationState.conversationId).toBe("hermes-session-123-claude-replay");
expect(first.conversationState.agentContinuationId).toBeTruthy();
expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId);
expect(first.conversationState.agentTaskType).toBe("vibe");
expect(first.conversationState).not.toHaveProperty("agentContinuationId");
expect(second.conversationState).not.toHaveProperty("agentTaskType");
expect(second.conversationState.history[0].userInputMessage.content).toBe(
first.conversationState.currentMessage.userInputMessage.content
);
@@ -84,7 +83,7 @@ describe("Claude → Kiro (direct route)", () => {
expect(out.systemPrompt).toContain(
"<thinking_mode>enabled</thinking_mode>"
);
expect(out.agentMode).toBe("vibe");
expect(out).not.toHaveProperty("agentMode");
});
it("does not send additionalModelRequestFields for Kiro models without effort support", () => {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

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");

View File

@@ -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)");
});
});

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",
});
});
});

View File

@@ -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);
});
});

View File

@@ -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
});
});

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"
);
});

View File

@@ -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");
});
});

View File

@@ -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([
{

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",

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();
}
}
});
});

View File

@@ -351,7 +351,7 @@ describe("handleImageGenerationCore", () => {
headers: expect.objectContaining({
authorization: "Bearer codex-token",
"chatgpt-account-id": "account-123",
version: "0.136.0",
version: "0.154.0",
}),
})
);
@@ -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(

View File

@@ -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,
]);
});

View File

@@ -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");
});
});
}

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 () => {

View File

@@ -628,7 +628,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
);

View File

@@ -4,9 +4,11 @@ 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,11 +24,14 @@ 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",
]);
});
@@ -91,7 +96,7 @@ 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"]) {
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();

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([]);
});
});

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
@@ -431,6 +438,21 @@ describe("normalizeMessages", () => {
]);
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", () => {
@@ -530,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}`);
});
});

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}`);
});
});

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();
});
});

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 () => []),

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { XiaomiMimoExecutor, __test__ } from "../../open-sse/executors/xiaomi-mimo.js";
import { getExecutor } from "../../open-sse/executors/index.js";
const { bareModel, COOKIE_KEY } = __test__;
const OPENAI_T = { runtimeTransport: { format: "openai", baseUrl: "https://api.xiaomimimo.com/v1/chat/completions" } };
const CLAUDE_T = { runtimeTransport: { format: "claude", baseUrl: "https://api.xiaomimimo.com/anthropic/v1/messages" } };
describe("xiaomi-mimo executor", () => {
let ex;
beforeEach(() => {
ex = new XiaomiMimoExecutor();
});
it("is registered for xiaomi-mimo", () => {
expect(getExecutor("xiaomi-mimo")).toBeInstanceOf(XiaomiMimoExecutor);
});
it("routes Preview models to the account-service route regardless of transport", () => {
const expected = "https://mimo-server-cn.xiaomimimo.com/api/route/chat/completions";
expect(ex.buildUrl("mimo-x-pro-preview", true, 0, OPENAI_T)).toBe(expected);
expect(ex.buildUrl("mimo-x-pro-preview", true, 0, CLAUDE_T)).toBe(expected);
// body.model arrives as `xiaomi/<id>` via upstreamModelId
expect(ex.buildUrl("xiaomi/mimo-x-flash-preview", true, 0, OPENAI_T)).toBe(expected);
});
it("keeps the sourceFormat-matched endpoint for cloud models", () => {
// Regression: a Claude client must reach /anthropic/v1/messages, not /v1/chat/completions.
expect(ex.buildUrl("mimo-v2.5-pro", true, 0, CLAUDE_T)).toBe(CLAUDE_T.runtimeTransport.baseUrl);
expect(ex.buildUrl("mimo-v2.5-pro", true, 0, OPENAI_T)).toBe(OPENAI_T.runtimeTransport.baseUrl);
});
it("authenticates Preview calls with the account cookie", () => {
const headers = ex.buildHeaders({ [COOKIE_KEY]: "serviceToken=abc", accessToken: "sk-x" }, true, "u", "mimo-x-pro-preview");
expect(headers.Cookie).toBe("serviceToken=abc");
expect(headers.Authorization).toBeUndefined();
});
it("authenticates cloud calls with the bearer key", () => {
const headers = ex.buildHeaders({ accessToken: "sk-x" }, true, "u", "mimo-v2.5-pro");
expect(headers.Authorization).toBe("Bearer sk-x");
expect(headers.Cookie).toBeUndefined();
});
it("fails fast when a Preview call has no account session", async () => {
await expect(
ex.execute({ model: "mimo-x-pro-preview", body: {}, stream: true, credentials: {}, log: null }),
).rejects.toThrow(/account session unavailable/);
});
it("flattens content-part arrays to plain strings", () => {
const out = ex.transformRequest(
"mimo-x-pro-preview",
{ messages: [{ role: "user", content: [{ type: "text", text: "a" }, { type: "text", text: "b" }] }] },
true,
{},
);
expect(out.messages[0].content).toBe("ab");
});
it("applies Preview defaults without overriding explicit values", () => {
const body = { messages: [{ role: "user", content: "hi" }], temperature: 0.2 };
const out = ex.transformRequest("mimo-x-pro-preview", body, true, {});
expect(out.temperature).toBe(0.2); // caller's value kept
expect(out.top_p).toBe(0.95); // default filled in
expect(out.max_tokens).toBe(4096);
});
it("leaves cloud bodies free of Preview defaults", () => {
const out = ex.transformRequest("mimo-v2.5-pro", { messages: [{ role: "user", content: "hi" }] }, true, {});
expect(out.thinking).toBeUndefined();
expect(out.max_tokens).toBeUndefined();
});
it("strips a provider/model prefix when testing preview ids", () => {
expect(bareModel("xiaomi/mimo-x-pro-preview")).toBe("mimo-x-pro-preview");
expect(bareModel("mimo-x-pro-preview")).toBe("mimo-x-pro-preview");
});
});

View File

@@ -0,0 +1,54 @@
/**
* Regression: the xiaomi-mimo OAuth session store must not retain sessions
* once the callback listener is down.
*
* Each /authorize registers a session holding an X25519 private key, keyed by a
* fresh state. Unlike trae/windsurf/zed (singleton session) this is a Map, so
* without an explicit clear every login attempt would leak a private key for
* the whole process lifetime.
*/
import { describe, it, expect } from "vitest";
import {
registerXiaomiMimoSession,
getXiaomiMimoSessionStatus,
clearXiaomiMimoSession,
stopXiaomiMimoProxy,
} from "../../src/lib/oauth/utils/server.js";
const KEY = Buffer.from("x25519-private-key-material");
describe("xiaomi-mimo OAuth session store", () => {
it("drops pending sessions when the proxy stops", () => {
registerXiaomiMimoSession({ state: "s1", privateKeyDer: KEY });
expect(getXiaomiMimoSessionStatus("s1")).not.toBeNull();
stopXiaomiMimoProxy();
expect(getXiaomiMimoSessionStatus("s1")).toBeNull();
});
it("drops every session, not just the last one", () => {
registerXiaomiMimoSession({ state: "a", privateKeyDer: KEY });
registerXiaomiMimoSession({ state: "b", privateKeyDer: KEY });
registerXiaomiMimoSession({ state: "c", privateKeyDer: KEY });
stopXiaomiMimoProxy();
for (const s of ["a", "b", "c"]) {
expect(getXiaomiMimoSessionStatus(s)).toBeNull();
}
});
it("ignores registrations with a missing state or key", () => {
expect(registerXiaomiMimoSession({ state: "", privateKeyDer: KEY })).toBe(false);
expect(registerXiaomiMimoSession({ state: "s", privateKeyDer: null })).toBe(false);
});
it("never exposes the private key to callers", () => {
registerXiaomiMimoSession({ state: "s1", privateKeyDer: KEY });
const view = getXiaomiMimoSessionStatus("s1");
expect(view).toEqual({ status: "pending", result: null, error: null });
expect(JSON.stringify(view)).not.toContain("privateKeyDer");
clearXiaomiMimoSession("s1");
});
});

View File

@@ -0,0 +1,152 @@
/**
* Regression: the poll-status/exchange session lifecycle for xiaomi-mimo.
*
* The original PR cleared the session inside poll-status, so the client's
* following POST /exchange always saw a missing session and returned 400 —
* the whole browser-OAuth fallback was dead. These tests pin the contract:
* - a finished session survives /poll-status until /exchange consumes it
* - a failed session is cleaned up by /poll-status itself
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("next/server", () => ({
NextResponse: {
json: (body, init) => ({
status: init?.status || 200,
body,
json: async () => body,
}),
},
}));
vi.mock("@/lib/oauth/providers", () => ({
getProvider: vi.fn(),
generateAuthData: vi.fn(),
exchangeTokens: vi.fn(),
requestDeviceCode: vi.fn(),
pollForToken: vi.fn(),
}));
vi.mock("@/models", () => ({
createProviderConnection: vi.fn(async (d) => ({ id: "conn-1", ...d })),
}));
vi.mock("open-sse/shared/mimoAccount.js", () => ({
readDesktopPassToken: vi.fn(async () => ({ passToken: "pt-abc", userId: "u1", cUserId: "c1" })),
}));
vi.mock("@/lib/oauth/utils/ideDetect", () => ({ detectIdeInstalled: vi.fn() }));
// Session store backing the mocked OAuth server helpers, so the test can assert
// on real lifecycle transitions rather than on call counts alone.
const sessions = new Map();
const stopped = { count: 0 };
vi.mock("@/lib/oauth/utils/server", () => {
const notUsed = () => { throw new Error("unexpected helper"); };
const noop = () => {};
return {
startCodexProxy: notUsed, stopCodexProxy: noop, registerCodexSession: noop,
getCodexSessionStatus: () => null, clearCodexSession: noop,
startXaiProxy: notUsed, stopXaiProxy: noop, registerXaiSession: noop,
getXaiSessionStatus: () => null, clearXaiSession: noop,
startTraeProxy: notUsed, stopTraeProxy: noop, registerTraeSession: noop,
getTraeSessionStatus: () => null, clearTraeSession: noop,
startWindsurfProxy: notUsed, stopWindsurfProxy: noop, registerWindsurfSession: noop,
getWindsurfSessionStatus: () => null, clearWindsurfSession: noop,
startZedProxy: notUsed, stopZedProxy: noop, registerZedSession: noop,
getZedSessionStatus: () => null, clearZedSession: noop,
startXiaomiMimoProxy: notUsed,
stopXiaomiMimoProxy: () => { stopped.count += 1; },
registerXiaomiMimoSession: () => {},
getXiaomiMimoSessionStatus: (state) => {
const s = sessions.get(state);
return s ? { status: s.status, result: s.result || null, error: s.error || null } : null;
},
clearXiaomiMimoSession: (state) => { sessions.delete(state); },
};
});
const { GET, POST } = await import("../../src/app/api/oauth/[provider]/[action]/route.js");
const get = (action, state) =>
GET(new Request(`http://localhost/api/oauth/xiaomi-mimo/${action}?state=${state}`), {
params: Promise.resolve({ provider: "xiaomi-mimo", action }),
});
const exchange = (state) =>
POST(
new Request("http://localhost/api/oauth/xiaomi-mimo/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state }),
}),
{ params: Promise.resolve({ provider: "xiaomi-mimo", action: "exchange" }) },
);
describe("xiaomi-mimo OAuth session lifecycle", () => {
beforeEach(() => {
sessions.clear();
stopped.count = 0;
});
it("keeps a finished session alive so /exchange can consume it", async () => {
sessions.set("st1", { status: "done", result: { uid: "u1", accessToken: "sk-x", baseUrl: "https://api.xiaomimimo.com/v1" } });
const poll = await get("poll-status", "st1");
expect(poll.status).toBe(200);
expect(await poll.json()).toMatchObject({ status: "done" });
// The bug: this used to be gone, making /exchange always 400.
expect(sessions.has("st1")).toBe(true);
const res = await exchange("st1");
expect(res.status).toBe(200);
expect((await res.json()).success).toBe(true);
});
it("clears the session once /exchange consumed it", async () => {
sessions.set("st1", { status: "done", result: { uid: "u1", accessToken: "sk-x" } });
await exchange("st1");
expect(sessions.has("st1")).toBe(false);
});
it("cleans up a failed session in poll-status and stops the proxy", async () => {
sessions.set("st2", { status: "error", error: "Could not decrypt with any pending session key" });
const poll = await get("poll-status", "st2");
expect(await poll.json()).toMatchObject({ status: "error" });
expect(sessions.has("st2")).toBe(false);
expect(stopped.count).toBe(1);
});
it("persists the Desktop passToken onto the connection (Preview models need it)", async () => {
const { createProviderConnection } = await import("@/models");
sessions.set("st3", { status: "done", result: { uid: "u1", accessToken: "sk-x" } });
await exchange("st3");
const arg = createProviderConnection.mock.calls.at(-1)[0];
expect(arg.provider).toBe("xiaomi-mimo");
expect(arg.providerSpecificData.mimoPassToken).toBe("pt-abc");
expect(arg.providerSpecificData.mimoUserId).toBe("u1");
});
it("still reports unknown for an unregistered state", async () => {
const poll = await get("poll-status", "nope");
expect(await poll.json()).toEqual({ status: "unknown" });
});
it("rejects /exchange without a state", async () => {
const res = await POST(
new Request("http://localhost/api/oauth/xiaomi-mimo/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}),
{ params: Promise.resolve({ provider: "xiaomi-mimo", action: "exchange" }) },
);
expect(res.status).toBe(400);
});
});