Merge remote-tracking branch 'upstream/master'
# Conflicts: # open-sse/handlers/chatCore.js # open-sse/providers/registry/index.js # open-sse/services/usage.js
This commit is contained in:
commit
eaf204b83c
114 files changed
+5587
-2184
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,49 @@
|
||||
// Regression test: claude → claude streaming passthrough must still decloak
|
||||
// tool names. translateRequest() cloaks client tool names with CLAUDE_TOOL_SUFFIX
|
||||
// for OAuth-cloaked Claude providers (cloakToolsOnOAuth) even when source and
|
||||
// target formats match; the same-format fast path in translateResponse() used
|
||||
// to return chunks untouched, leaking the suffixed name (e.g. "run_code_ide")
|
||||
// to the client, which then rejected the call as an unknown tool.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import "./registerAll.js";
|
||||
import { translateResponse } from "../../open-sse/translator/index.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
import { CLAUDE_TOOL_SUFFIX } from "../../open-sse/config/appConstants.js";
|
||||
|
||||
const CLOAKED = "run_code" + CLAUDE_TOOL_SUFFIX;
|
||||
|
||||
const toolUseStart = (name) => ({
|
||||
type: "content_block_start",
|
||||
index: 1,
|
||||
content_block: { type: "tool_use", id: "toolu_01XYZ", name, input: {} }
|
||||
});
|
||||
|
||||
describe("Claude → Claude streaming passthrough (OAuth tool cloak)", () => {
|
||||
const state = { toolNameMap: new Map([[CLOAKED, "run_code"]]) };
|
||||
|
||||
it("restores the original tool name on tool_use content_block_start", () => {
|
||||
const [out] = translateResponse(FORMATS.CLAUDE, FORMATS.CLAUDE, toolUseStart(CLOAKED), state);
|
||||
expect(out.content_block.name).toBe("run_code");
|
||||
});
|
||||
|
||||
it("leaves uncloaked chunks untouched (identity passthrough)", () => {
|
||||
const chunk = toolUseStart("Bash"); // decoy name, not in the map
|
||||
const [out] = translateResponse(FORMATS.CLAUDE, FORMATS.CLAUDE, chunk, state);
|
||||
expect(out).toBe(chunk);
|
||||
|
||||
const textChunk = { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hi" } };
|
||||
const [outText] = translateResponse(FORMATS.CLAUDE, FORMATS.CLAUDE, textChunk, state);
|
||||
expect(outText).toBe(textChunk);
|
||||
});
|
||||
|
||||
it("is a no-op when no cloak map is present", () => {
|
||||
const chunk = toolUseStart(CLOAKED);
|
||||
const [out] = translateResponse(FORMATS.CLAUDE, FORMATS.CLAUDE, chunk, {});
|
||||
expect(out).toBe(chunk);
|
||||
});
|
||||
|
||||
it("tolerates the null flush chunk", () => {
|
||||
const [out] = translateResponse(FORMATS.CLAUDE, FORMATS.CLAUDE, null, state);
|
||||
expect(out).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -58,6 +58,18 @@ describe("extractThinking", () => {
|
||||
it("no intent → null", () => {
|
||||
expect(extractThinking({ messages: [] })).toBeNull();
|
||||
});
|
||||
it("reasoning_effort wins over thinking:{type:enabled} (no budget)", () => {
|
||||
expect(extractThinking({
|
||||
thinking: { type: "enabled" },
|
||||
reasoning_effort: "high",
|
||||
})).toEqual({ mode: "level", level: "high" });
|
||||
});
|
||||
it("reasoning.effort wins over thinking:{type:enabled} (no budget)", () => {
|
||||
expect(extractThinking({
|
||||
thinking: { type: "enabled" },
|
||||
reasoning: { effort: "medium" },
|
||||
})).toEqual({ mode: "level", level: "medium" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyThinking per provider format", () => {
|
||||
@@ -114,6 +126,27 @@ describe("applyThinking per provider format", () => {
|
||||
expect(out.enable_thinking).toBe(false);
|
||||
expect(out.thinking).toBeUndefined();
|
||||
});
|
||||
it.each([
|
||||
["high", "high"],
|
||||
["max", "max"],
|
||||
["xhigh", "max"],
|
||||
["low", "low"],
|
||||
["medium", "high"],
|
||||
["minimal", "low"],
|
||||
])("GLM-5.3 %s → reasoning_effort=%s (low|high|max only, per z.ai docs)", (input, expected) => {
|
||||
const out = apply("openai", "glm-5.3", { reasoning_effort: input }, "glm-cn");
|
||||
expect(out.thinking).toEqual({ type: "enabled" });
|
||||
expect(out.reasoning_effort).toBe(expected);
|
||||
});
|
||||
it("GLM-5.2 also gets reasoning_effort (supported from 5.2 onward)", () => {
|
||||
const out = apply("openai", "glm-5.2", { reasoning_effort: "low" }, "glm-cn");
|
||||
expect(out.reasoning_effort).toBe("low");
|
||||
});
|
||||
it("GLM-4.7 (pre-5.2) does not get reasoning_effort — z.ai ignores it", () => {
|
||||
const out = apply("openai", "glm-4.7", { reasoning_effort: "low" }, "glm-cn");
|
||||
expect(out.thinking).toEqual({ type: "enabled" });
|
||||
expect(out.reasoning_effort).toBeUndefined();
|
||||
});
|
||||
it("Qwen on → enable_thinking + thinking_budget", () => {
|
||||
const out = apply("openai", "qwen3-max", { reasoning_effort: "medium" }, "qwen");
|
||||
expect(out.enable_thinking).toBe(true);
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getProviderConnections: vi.fn(),
|
||||
getSettings: vi.fn(),
|
||||
resolveConnectionProxyConfig: vi.fn(),
|
||||
getAntigravityUsage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/localDb", () => ({
|
||||
getProviderConnections: mocks.getProviderConnections,
|
||||
getSettings: mocks.getSettings,
|
||||
getProxyPools: vi.fn(),
|
||||
validateApiKey: vi.fn(),
|
||||
updateProviderConnection: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/network/connectionProxy", () => ({
|
||||
resolveConnectionProxyConfig: mocks.resolveConnectionProxyConfig,
|
||||
pickProxyPoolId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/shared/constants/providers.js", () => ({
|
||||
FREE_PROVIDERS: {},
|
||||
resolveProviderId: (provider) => provider,
|
||||
}));
|
||||
vi.mock("open-sse/services/usage/google.js", () => ({
|
||||
getAntigravityUsage: mocks.getAntigravityUsage,
|
||||
}));
|
||||
vi.mock("@/sse/utils/logger.js", () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn() }));
|
||||
|
||||
const { getAntigravityQuotaCache, handleAntigravityQuotaError, refreshAntigravityQuota } = await import("@/sse/services/antigravityQuota.js");
|
||||
const { getProviderCredentials } = await import("@/sse/services/auth.js");
|
||||
|
||||
const MODEL = "claude-opus-4-6-thinking";
|
||||
const FUTURE_RESET = "2026-09-01T00:00:00.000Z";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
getAntigravityQuotaCache().clear();
|
||||
mocks.resolveConnectionProxyConfig.mockResolvedValue({});
|
||||
mocks.getSettings.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("Antigravity quota-aware routing", () => {
|
||||
it("records exhausted upstream quota after 429 and returns its exact reset time", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
|
||||
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
|
||||
[MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET },
|
||||
} });
|
||||
|
||||
try {
|
||||
await expect(handleAntigravityQuotaError("ag-a", 429, MODEL, "token", {}))
|
||||
.resolves.toBe(Date.parse(FUTURE_RESET));
|
||||
expect(getAntigravityQuotaCache().get("ag-a")[MODEL]).toEqual({
|
||||
remainingPercentage: 0,
|
||||
resetAt: FUTURE_RESET,
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("skips exhausted account/model and selects the next account", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
|
||||
mocks.getProviderConnections.mockResolvedValue([
|
||||
{ id: "ag-a", email: "a@example.com", isActive: true },
|
||||
{ id: "ag-b", email: "b@example.com", isActive: true },
|
||||
]);
|
||||
getAntigravityQuotaCache().set("ag-a", {
|
||||
[MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET },
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(getProviderCredentials("antigravity", null, MODEL)).resolves.toMatchObject({
|
||||
connectionId: "ag-b",
|
||||
connectionName: "b@example.com",
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports retry time when every account is cache-blocked", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
|
||||
mocks.getProviderConnections.mockResolvedValue([{ id: "ag-a", email: "a@example.com", isActive: true }]);
|
||||
getAntigravityQuotaCache().set("ag-a", {
|
||||
[MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET },
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(getProviderCredentials("antigravity", null, MODEL)).resolves.toMatchObject({
|
||||
allRateLimited: true,
|
||||
retryAfter: FUTURE_RESET,
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("lets account back into rotation once reset time has passed", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-09-01T00:00:01.000Z"));
|
||||
mocks.getProviderConnections.mockResolvedValue([{ id: "ag-a", email: "a@example.com", isActive: true }]);
|
||||
getAntigravityQuotaCache().set("ag-a", {
|
||||
[MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET },
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(getProviderCredentials("antigravity", null, MODEL)).resolves.toMatchObject({
|
||||
connectionId: "ag-a",
|
||||
connectionName: "a@example.com",
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("coalesces concurrent quota refreshes for one account", async () => {
|
||||
let resolveUsage;
|
||||
mocks.getAntigravityUsage.mockReturnValue(new Promise(resolve => { resolveUsage = resolve; }));
|
||||
|
||||
const first = refreshAntigravityQuota("ag-concurrent", "token", {});
|
||||
const second = refreshAntigravityQuota("ag-concurrent", "token", {});
|
||||
resolveUsage({ quotas: { [MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET } } });
|
||||
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([
|
||||
{ [MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET } },
|
||||
{ [MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET } },
|
||||
]);
|
||||
expect(mocks.getAntigravityUsage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("preserves strict proxy policy for usage refresh", async () => {
|
||||
mocks.resolveConnectionProxyConfig.mockResolvedValue({ strictProxy: true });
|
||||
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {} });
|
||||
|
||||
await refreshAntigravityQuota("ag-strict-proxy", "token", {});
|
||||
|
||||
expect(mocks.getAntigravityUsage).toHaveBeenCalledWith("token", {}, expect.objectContaining({
|
||||
strictProxy: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it("keeps known cache when quota endpoint returns an error payload", async () => {
|
||||
const cached = { [MODEL]: { remainingPercentage: 0, resetAt: FUTURE_RESET } };
|
||||
getAntigravityQuotaCache().set("ag-error-response", cached);
|
||||
mocks.getAntigravityUsage.mockResolvedValue({ message: "Unauthorized", quotas: {} });
|
||||
|
||||
await expect(refreshAntigravityQuota("ag-error-response", "token", {})).resolves.toBeNull();
|
||||
expect(getAntigravityQuotaCache().get("ag-error-response")).toBe(cached);
|
||||
});
|
||||
|
||||
it("throttles failed refresh attempts for 30 seconds", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
|
||||
mocks.getAntigravityUsage.mockRejectedValue(new Error("usage unavailable"));
|
||||
|
||||
try {
|
||||
await refreshAntigravityQuota("ag-failed-refresh", "token", {});
|
||||
await refreshAntigravityQuota("ag-failed-refresh", "token", {});
|
||||
expect(mocks.getAntigravityUsage).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await refreshAntigravityQuota("ag-failed-refresh", "token", {});
|
||||
expect(mocks.getAntigravityUsage).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { canonicalizeUsage, extractUsage, mergeUsage } from "../../open-sse/utils/usageTracking.js";
|
||||
import { calculateCostFromTokens } from "../../open-sse/providers/pricing.js";
|
||||
import { toOpenAIUsage } from "../../open-sse/translator/concerns/usage.js";
|
||||
import { buildUsage, toOpenAIUsage } from "../../open-sse/translator/concerns/usage.js";
|
||||
|
||||
// Canonical convention (single source of truth for storage + cost):
|
||||
// prompt_tokens = total input INCLUDING cache read + cache creation
|
||||
@@ -49,6 +49,18 @@ describe("canonicalizeUsage", () => {
|
||||
expect(out.reasoning_tokens).toBe(40);
|
||||
});
|
||||
|
||||
it("reads cached_tokens from the nested buildUsage() shape", () => {
|
||||
// buildUsage() only emits cache reads under prompt_tokens_details. The
|
||||
// Responses translator overwrites state.usage with that shape on
|
||||
// response.completed, so a top-level-only read silently drops the cache
|
||||
// count for every Responses provider (codex, grok-cli, ...).
|
||||
const out = canonicalizeUsage(
|
||||
buildUsage({ promptTokens: 330, completionTokens: 50, totalTokens: 380, cachedTokens: 200 })
|
||||
);
|
||||
expect(out.prompt_tokens).toBe(330);
|
||||
expect(out.cached_tokens).toBe(200);
|
||||
});
|
||||
|
||||
it("handles no-cache usage", () => {
|
||||
const out = canonicalizeUsage({ prompt_tokens: 100, completion_tokens: 50 });
|
||||
expect(out.prompt_tokens).toBe(100);
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
*
|
||||
* Tests cover:
|
||||
* - cloakClaudeTools() - tool renaming and forced tool_choice suffixing
|
||||
* - decloakStreamChunk() - restoring tool names in streamed Claude SSE events
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { cloakClaudeTools } from "../../open-sse/utils/claudeCloaking.js";
|
||||
import { cloakClaudeTools, decloakStreamChunk } from "../../open-sse/utils/claudeCloaking.js";
|
||||
import { CLAUDE_TOOL_SUFFIX } from "../../open-sse/config/appConstants.js";
|
||||
|
||||
describe("cloakClaudeTools", () => {
|
||||
@@ -74,3 +75,44 @@ describe("cloakClaudeTools", () => {
|
||||
expect(toolNameMap).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("decloakStreamChunk", () => {
|
||||
// Cloaked exactly as cloakClaudeTools() does on the request side
|
||||
const toolNameMap = new Map([["run_code" + CLAUDE_TOOL_SUFFIX, "run_code"]]);
|
||||
|
||||
const toolUseStart = (name) => ({
|
||||
type: "content_block_start",
|
||||
index: 1,
|
||||
content_block: { type: "tool_use", id: "toolu_01abc", name, input: {} }
|
||||
});
|
||||
|
||||
it("restores the original name on a tool_use content_block_start", () => {
|
||||
const out = decloakStreamChunk(toolUseStart("run_code" + CLAUDE_TOOL_SUFFIX), toolNameMap);
|
||||
expect(out.content_block.name).toBe("run_code");
|
||||
});
|
||||
|
||||
it("does not mutate the input chunk", () => {
|
||||
const chunk = toolUseStart("run_code" + CLAUDE_TOOL_SUFFIX);
|
||||
decloakStreamChunk(chunk, toolNameMap);
|
||||
expect(chunk.content_block.name).toBe("run_code" + CLAUDE_TOOL_SUFFIX);
|
||||
});
|
||||
|
||||
it("passes through names the map does not know (e.g. decoy tools)", () => {
|
||||
const chunk = toolUseStart("Bash");
|
||||
expect(decloakStreamChunk(chunk, toolNameMap)).toBe(chunk);
|
||||
});
|
||||
|
||||
it("passes through non-tool_use events unchanged", () => {
|
||||
const textStart = { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } };
|
||||
expect(decloakStreamChunk(textStart, toolNameMap)).toBe(textStart);
|
||||
|
||||
const delta = { type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: "{}" } };
|
||||
expect(decloakStreamChunk(delta, toolNameMap)).toBe(delta);
|
||||
});
|
||||
|
||||
it("tolerates null chunks and missing maps (stream flush path)", () => {
|
||||
expect(decloakStreamChunk(null, toolNameMap)).toBeNull();
|
||||
expect(decloakStreamChunk(toolUseStart("run_code" + CLAUDE_TOOL_SUFFIX), null).content_block.name).toBe("run_code" + CLAUDE_TOOL_SUFFIX);
|
||||
expect(decloakStreamChunk(toolUseStart("run_code" + CLAUDE_TOOL_SUFFIX), new Map()).content_block.name).toBe("run_code" + CLAUDE_TOOL_SUFFIX);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||
|
||||
describe("Codex Spark Quota Tracking (#3431)", () => {
|
||||
it("correctly normalizes spark_session and spark_weekly quotas with display labels", () => {
|
||||
const mockCodexUsage = {
|
||||
plan: "team",
|
||||
quotas: {
|
||||
session: { used: 20, total: 100, remaining: 80, resetAt: "2026-08-22T05:00:00.000Z" },
|
||||
weekly: { used: 40, total: 100, remaining: 60, resetAt: "2026-08-28T05:00:00.000Z" },
|
||||
review_session: { used: 0, total: 100, remaining: 100, resetAt: "2026-08-22T05:00:00.000Z" },
|
||||
spark_session: { used: 12, total: 100, remaining: 88, resetAt: "2026-08-22T05:00:00.000Z" },
|
||||
spark_weekly: { used: 25, total: 100, remaining: 75, resetAt: "2026-08-28T05:00:00.000Z" },
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = parseQuotaData("codex", mockCodexUsage);
|
||||
|
||||
const sparkSession = parsed.find((q) => q.name === "Spark (5h)");
|
||||
const sparkWeekly = parsed.find((q) => q.name === "Spark (Weekly)");
|
||||
const session = parsed.find((q) => q.name === "5h");
|
||||
const weekly = parsed.find((q) => q.name === "Weekly");
|
||||
|
||||
expect(sparkSession).toBeDefined();
|
||||
expect(sparkSession.used).toBe(12);
|
||||
expect(sparkSession.remaining).toBe(88);
|
||||
|
||||
expect(sparkWeekly).toBeDefined();
|
||||
expect(sparkWeekly.used).toBe(25);
|
||||
expect(sparkWeekly.remaining).toBe(75);
|
||||
|
||||
expect(session).toBeDefined();
|
||||
expect(session.used).toBe(20);
|
||||
expect(weekly).toBeDefined();
|
||||
expect(weekly.used).toBe(40);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
parseCommandCodeError,
|
||||
inspectAndWrapCommandCodeResponse,
|
||||
CommandCodeExecutor,
|
||||
} from "../../open-sse/executors/commandcode.js";
|
||||
import { handleComboChat } from "../../open-sse/services/combo.js";
|
||||
|
||||
function createNdjsonStream(lines) {
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
for (const line of lines) {
|
||||
controller.enqueue(encoder.encode(typeof line === "string" ? line : JSON.stringify(line) + "\n"));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("parseCommandCodeError", () => {
|
||||
it("parses user exact error payload with statusCode 503 and isRetryable", () => {
|
||||
const event = {
|
||||
type: "error",
|
||||
error: {
|
||||
type: "server_error",
|
||||
message: "Service temporarily unavailable. Please try again shortly.",
|
||||
statusCode: 503,
|
||||
isRetryable: true,
|
||||
},
|
||||
};
|
||||
const parsed = parseCommandCodeError(event);
|
||||
expect(parsed.statusCode).toBe(503);
|
||||
expect(parsed.message).toBe("Service temporarily unavailable. Please try again shortly.");
|
||||
expect(parsed.type).toBe("server_error");
|
||||
});
|
||||
|
||||
it("handles string error message", () => {
|
||||
const event = {
|
||||
type: "error",
|
||||
message: "Rate limit exceeded. Please wait 30s.",
|
||||
};
|
||||
const parsed = parseCommandCodeError(event);
|
||||
expect(parsed.statusCode).toBe(429);
|
||||
expect(parsed.message).toBe("Rate limit exceeded. Please wait 30s.");
|
||||
});
|
||||
|
||||
it("handles plain error string in error property", () => {
|
||||
const event = {
|
||||
type: "error",
|
||||
error: "Unauthorized access",
|
||||
};
|
||||
const parsed = parseCommandCodeError(event);
|
||||
expect(parsed.statusCode).toBe(401);
|
||||
expect(parsed.message).toBe("Unauthorized access");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inspectAndWrapCommandCodeResponse", () => {
|
||||
it("converts initial upstream 200 with error event to 503 Response", async () => {
|
||||
const ndjsonBody = createNdjsonStream([
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error: {
|
||||
type: "server_error",
|
||||
message: "Service temporarily unavailable. Please try again shortly.",
|
||||
statusCode: 503,
|
||||
isRetryable: true,
|
||||
},
|
||||
}) + "\n",
|
||||
]);
|
||||
|
||||
const fakeResponse = new Response(ndjsonBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
|
||||
const result = await inspectAndWrapCommandCodeResponse(fakeResponse, "poolside/laguna-s-2.1-free");
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.status).toBe(503);
|
||||
|
||||
const body = await result.json();
|
||||
expect(body.error.message).toContain("Service temporarily unavailable");
|
||||
expect(body.error.code).toBe(503);
|
||||
});
|
||||
|
||||
it("converts initial upstream 200 with start/start-step followed by error to 503 Response", async () => {
|
||||
const ndjsonBody = createNdjsonStream([
|
||||
JSON.stringify({ type: "start" }) + "\n",
|
||||
JSON.stringify({ type: "start-step" }) + "\n",
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error: {
|
||||
type: "server_error",
|
||||
message: "Service temporarily unavailable. Please try again shortly.",
|
||||
statusCode: 503,
|
||||
isRetryable: true,
|
||||
},
|
||||
}) + "\n",
|
||||
]);
|
||||
|
||||
const fakeResponse = new Response(ndjsonBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
|
||||
const result = await inspectAndWrapCommandCodeResponse(fakeResponse, "poolside/laguna-s-2.1-free");
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.status).toBe(503);
|
||||
|
||||
const body = await result.json();
|
||||
expect(body.error.message).toContain("Service temporarily unavailable");
|
||||
});
|
||||
|
||||
it("streams successful responses when content is emitted", async () => {
|
||||
const ndjsonBody = createNdjsonStream([
|
||||
JSON.stringify({ type: "start" }) + "\n",
|
||||
JSON.stringify({ type: "text-delta", text: "Hello from Laguna" }) + "\n",
|
||||
JSON.stringify({ type: "finish" }) + "\n",
|
||||
]);
|
||||
|
||||
const fakeResponse = new Response(ndjsonBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
|
||||
const result = await inspectAndWrapCommandCodeResponse(fakeResponse, "poolside/laguna-s-2.1-free");
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
const text = await result.text();
|
||||
expect(text).toContain("Hello from Laguna");
|
||||
expect(text).toContain("data: [DONE]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommandCode in Combo Fallback", () => {
|
||||
it("automatically falls back to next model when commandcode returns 503 error", async () => {
|
||||
const log = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
|
||||
const handleSingleModel = vi.fn(async (body, modelStr) => {
|
||||
if (modelStr === "commandcode/poolside/laguna-s-2.1-free") {
|
||||
// Simulated failed CommandCode response
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Service temporarily unavailable. Please try again shortly.",
|
||||
type: "server_error",
|
||||
code: 503,
|
||||
},
|
||||
}),
|
||||
{ status: 503, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
if (modelStr === "openai/gpt-4o-mini") {
|
||||
// Fallback model succeeds
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "chatcmpl-test",
|
||||
choices: [{ message: { role: "assistant", content: "Fallback success!" } }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
|
||||
const comboResponse = await handleComboChat({
|
||||
body: { messages: [{ role: "user", content: "Hello" }] },
|
||||
models: ["commandcode/poolside/laguna-s-2.1-free", "openai/gpt-4o-mini"],
|
||||
handleSingleModel,
|
||||
log,
|
||||
comboName: "test-combo",
|
||||
comboStrategy: "fallback",
|
||||
});
|
||||
|
||||
expect(comboResponse.ok).toBe(true);
|
||||
expect(comboResponse.status).toBe(200);
|
||||
|
||||
const data = await comboResponse.json();
|
||||
expect(data.choices[0].message.content).toBe("Fallback success!");
|
||||
expect(handleSingleModel).toHaveBeenCalledTimes(2);
|
||||
expect(handleSingleModel).toHaveBeenNthCalledWith(1, expect.anything(), "commandcode/poolside/laguna-s-2.1-free");
|
||||
expect(handleSingleModel).toHaveBeenNthCalledWith(2, expect.anything(), "openai/gpt-4o-mini");
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../open-sse/config/ru
|
||||
import mimoFree from "../../open-sse/providers/registry/mimo-free.js";
|
||||
import opencode from "../../open-sse/providers/registry/opencode.js";
|
||||
import antigravity from "../../open-sse/providers/registry/antigravity.js";
|
||||
import { OpenCodeExecutor } from "../../open-sse/executors/opencode.js";
|
||||
|
||||
describe("compat base URLs / version", () => {
|
||||
it("OPENAI_COMPAT_BASE", () => {
|
||||
@@ -46,3 +47,36 @@ describe("antigravity retry (intentional change: 429=6, 503=3)", () => {
|
||||
expect(antigravity.transport.retry["503"].attempts).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenCode Free endpoint routing", () => {
|
||||
const MUSE = "muse-spark-1.2-contributor-free";
|
||||
|
||||
it("declares the Responses format only on the Muse Spark model", () => {
|
||||
expect(opencode.transport.format).toBeUndefined();
|
||||
const muse = opencode.models.find((m) => m.id === MUSE);
|
||||
expect(muse?.targetFormat).toBe("openai-responses");
|
||||
});
|
||||
|
||||
it("routes Muse Spark to /responses and every other model to /chat/completions", () => {
|
||||
const executor = new OpenCodeExecutor();
|
||||
expect(executor.buildUrl(MUSE)).toBe("https://opencode.ai/zen/v1/responses");
|
||||
expect(executor.buildUrl(`${MUSE}(xhigh)`)).toBe("https://opencode.ai/zen/v1/responses");
|
||||
expect(executor.buildUrl("big-pickle")).toBe("https://opencode.ai/zen/v1/chat/completions");
|
||||
expect(executor.buildUrl("hy3-free")).toBe("https://opencode.ai/zen/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("normalizes Chat token/thinking fields only for the Responses model", () => {
|
||||
const executor = new OpenCodeExecutor();
|
||||
const muse = { max_tokens: 4096, reasoning_effort: "high" };
|
||||
executor.transformRequest(MUSE, muse, true, {});
|
||||
expect(muse.max_output_tokens).toBe(4096);
|
||||
expect(muse.max_tokens).toBeUndefined();
|
||||
expect(muse.reasoning).toEqual({ effort: "high", summary: "auto" });
|
||||
|
||||
const chat = { max_tokens: 4096, reasoning_effort: "high" };
|
||||
executor.transformRequest("big-pickle", chat, true, {});
|
||||
expect(chat.max_tokens).toBe(4096);
|
||||
expect(chat.max_output_tokens).toBeUndefined();
|
||||
expect(chat.reasoning_effort).toBe("high");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRequire } from "node:module";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { getModelUpstreamId } from "../../open-sse/config/providerModels.js";
|
||||
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js";
|
||||
import { applyThinking, stripThinkingSuffix } from "../../open-sse/translator/concerns/thinkingUnified.js";
|
||||
import gemini from "../../open-sse/providers/registry/gemini.js";
|
||||
import { MODEL_PRICING } from "../../open-sse/providers/pricing.js";
|
||||
import { MITM_TOOLS } from "../../src/shared/constants/cliTools.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const mitmConfig = require("../../src/mitm/config.js");
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Gemini 3.7 Antigravity tiers", () => {
|
||||
it.each(["high", "medium", "low"])(
|
||||
"maps the %s tier to the shared upstream model with matching thinking level",
|
||||
(tier) => {
|
||||
const publicModel = `gemini-3.7-flash-${tier}`;
|
||||
const upstreamModel = getModelUpstreamId("ag", publicModel);
|
||||
const body = {
|
||||
model: stripThinkingSuffix(upstreamModel),
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: "hello" }] }],
|
||||
generationConfig: {},
|
||||
},
|
||||
};
|
||||
|
||||
applyThinking("antigravity", upstreamModel, body, "antigravity");
|
||||
const finalBody = new AntigravityExecutor().transformRequest(
|
||||
publicModel,
|
||||
body,
|
||||
true,
|
||||
{ projectId: "project", connectionId: "connection" }
|
||||
);
|
||||
|
||||
expect(upstreamModel).toBe(`gemini-3.7-flash-tiered(${tier})`);
|
||||
expect(finalBody.model).toBe("gemini-3.7-flash-tiered");
|
||||
expect(finalBody.request.generationConfig.thinkingConfig).toEqual({
|
||||
thinkingLevel: tier,
|
||||
includeThoughts: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("Gemini 3.7 MITM model extraction", () => {
|
||||
it.each(["high", "medium", "low"])("extracts the %s thinking tier for gemini-3.7-flash-tiered", (tier) => {
|
||||
const body = Buffer.from(JSON.stringify({
|
||||
request: { generationConfig: { thinkingConfig: { thinkingLevel: tier } } },
|
||||
}));
|
||||
|
||||
expect(mitmConfig.extractModel(
|
||||
"/v1internal/models/gemini-3.7-flash-tiered:streamGenerateContent",
|
||||
body
|
||||
)).toBe(`gemini-3.7-flash-${tier}`);
|
||||
});
|
||||
|
||||
it("defaults invalid or missing thinking levels to medium", () => {
|
||||
const body = Buffer.from(JSON.stringify({
|
||||
request: { generationConfig: { thinkingConfig: { thinkingLevel: "unknown" } } },
|
||||
}));
|
||||
|
||||
expect(mitmConfig.extractModel(
|
||||
"/v1internal/models/gemini-3.7-flash-tiered:streamGenerateContent",
|
||||
body
|
||||
)).toBe("gemini-3.7-flash-medium");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Gemini 3.7 MITM tools and catalog", () => {
|
||||
it("includes gemini-3.7-flash tiers in MITM_TOOLS defaultModels", () => {
|
||||
const defaultModelIds = MITM_TOOLS.antigravity.defaultModels.map((m) => m.id);
|
||||
expect(defaultModelIds).toContain("gemini-3.7-flash-high");
|
||||
expect(defaultModelIds).toContain("gemini-3.7-flash-medium");
|
||||
expect(defaultModelIds).toContain("gemini-3.7-flash-low");
|
||||
});
|
||||
|
||||
it("exposes the direct Gemini 3.7 API models and pricing", () => {
|
||||
const ids = gemini.models.map((model) => model.id);
|
||||
expect(ids).toContain("gemini-3.7-flash");
|
||||
expect(MODEL_PRICING["gemini-3.7-flash"]).toMatchObject({ input: 1.5, output: 7.5 });
|
||||
});
|
||||
|
||||
it("keeps the standalone CLI Antigravity catalog synchronized", () => {
|
||||
const source = readFileSync(join(here, "../../cli/src/cli/menus/providers.js"), "utf8");
|
||||
const agCatalog = source.match(/\n ag: \[([\s\S]*?)\n \],/)?.[1] || "";
|
||||
|
||||
expect(agCatalog).toContain("gemini-3.7-flash-high");
|
||||
expect(agCatalog).toContain("gemini-3.7-flash-medium");
|
||||
expect(agCatalog).toContain("gemini-3.7-flash-low");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||
proxyAwareFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
|
||||
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||
import { getGlmUsage } from "../../open-sse/services/usage/glm.js";
|
||||
import {
|
||||
USAGE_SUPPORTED_PROVIDERS,
|
||||
USAGE_APIKEY_PROVIDERS,
|
||||
} from "../../src/shared/constants/providers.js";
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const SAMPLE_GLM_CREDIT_USAGE = {
|
||||
code: 200,
|
||||
msg: "Operation successful",
|
||||
data: {
|
||||
limits: [
|
||||
{
|
||||
type: "CREDIT_LIMIT",
|
||||
unit: 3,
|
||||
number: 5,
|
||||
usage: 2000,
|
||||
currentValue: 0,
|
||||
remaining: 1999,
|
||||
percentage: 25,
|
||||
nextResetTime: 1787905548392,
|
||||
},
|
||||
{
|
||||
type: "CREDIT_LIMIT",
|
||||
unit: 6,
|
||||
number: 1,
|
||||
usage: 10000,
|
||||
currentValue: 0,
|
||||
remaining: 9999,
|
||||
percentage: 10,
|
||||
nextResetTime: 1788492142997,
|
||||
},
|
||||
],
|
||||
level: "lite",
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
const SAMPLE_GLM_TOKENS_USAGE = {
|
||||
code: 200,
|
||||
msg: "Operation successful",
|
||||
data: {
|
||||
limits: [
|
||||
{
|
||||
type: "TOKENS_LIMIT",
|
||||
percentage: 40,
|
||||
nextResetTime: 1787905548392,
|
||||
},
|
||||
],
|
||||
level: "standard",
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
describe("glm registry usage flags", () => {
|
||||
it("is listed for apikey quota dashboard", () => {
|
||||
expect(USAGE_SUPPORTED_PROVIDERS).toContain("glm");
|
||||
expect(USAGE_SUPPORTED_PROVIDERS).toContain("glm-cn");
|
||||
expect(USAGE_APIKEY_PROVIDERS).toContain("glm");
|
||||
expect(USAGE_APIKEY_PROVIDERS).toContain("glm-cn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGlmUsage and getUsageForProvider(glm)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("handles CREDIT_LIMIT with session 5h and weekly 7d quotas", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(SAMPLE_GLM_CREDIT_USAGE));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "glm",
|
||||
apiKey: "glm-key-123",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.plan).toBe("Lite");
|
||||
expect(usage.quotas["Session (5h)"]).toEqual({
|
||||
used: 25,
|
||||
total: 100,
|
||||
remaining: 75,
|
||||
remainingPercentage: 75,
|
||||
resetAt: new Date(1787905548392).toISOString(),
|
||||
unlimited: false,
|
||||
});
|
||||
expect(usage.quotas["Weekly (7d)"]).toEqual({
|
||||
used: 100 ? 10 : 10,
|
||||
total: 100,
|
||||
remaining: 90,
|
||||
remainingPercentage: 90,
|
||||
resetAt: new Date(1788492142997).toISOString(),
|
||||
unlimited: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles TOKENS_LIMIT quotas", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(SAMPLE_GLM_TOKENS_USAGE));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "glm-cn",
|
||||
apiKey: "glm-cn-key",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.plan).toBe("Standard");
|
||||
expect(usage.quotas["Tokens"]).toEqual({
|
||||
used: 40,
|
||||
total: 100,
|
||||
remaining: 60,
|
||||
remainingPercentage: 60,
|
||||
resetAt: new Date(1787905548392).toISOString(),
|
||||
unlimited: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles fallback key for custom limit units", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
code: 200,
|
||||
data: {
|
||||
limits: [
|
||||
{
|
||||
type: "CREDIT_LIMIT",
|
||||
unit: 99,
|
||||
number: 12,
|
||||
percentage: 5,
|
||||
nextResetTime: 0,
|
||||
},
|
||||
],
|
||||
level: "pro",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const usage = await getGlmUsage("glm-key", "glm");
|
||||
expect(usage.plan).toBe("Pro");
|
||||
expect(usage.quotas["Limit (12)"]).toEqual({
|
||||
used: 5,
|
||||
total: 100,
|
||||
remaining: 95,
|
||||
remainingPercentage: 95,
|
||||
resetAt: null,
|
||||
unlimited: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces invalid key message on 401", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "glm",
|
||||
apiKey: "invalid-key",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/invalid or expired/i);
|
||||
});
|
||||
|
||||
it("handles non-200 error response", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "server error" }, 500));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "glm",
|
||||
apiKey: "valid-key",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/GLM quota API error \(500\)/);
|
||||
});
|
||||
|
||||
it("returns message when apiKey is missing", async () => {
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "glm",
|
||||
apiKey: "",
|
||||
});
|
||||
|
||||
expect(usage.message).toBe("GLM API key not available.");
|
||||
});
|
||||
});
|
||||
@@ -202,6 +202,101 @@ describe("compressWithHeadroom", () => {
|
||||
expect(stats).toBeNull();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("timeout normalization", () => {
|
||||
const mockResponse = JSON.stringify({
|
||||
messages: [{ role: "user", content: "short" }],
|
||||
tokens_before: 100,
|
||||
tokens_after: 20,
|
||||
tokens_saved: 80,
|
||||
});
|
||||
|
||||
function makeSuccessfulFetch() {
|
||||
global.fetch = vi.fn(async () =>
|
||||
new Response(mockResponse, { status: 200 })
|
||||
);
|
||||
}
|
||||
|
||||
function captureTimeoutCalls() {
|
||||
const calls = [];
|
||||
vi.spyOn(AbortSignal, "timeout").mockImplementation((ms) => {
|
||||
calls.push(ms);
|
||||
const controller = new AbortController();
|
||||
return controller.signal;
|
||||
});
|
||||
return calls;
|
||||
}
|
||||
|
||||
it("passes a valid positive timeout to AbortSignal.timeout", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: 5000 });
|
||||
|
||||
expect(calls).toContain(5000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is null", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: null });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is 0", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: 0 });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is negative", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: -100 });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is NaN", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: NaN });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is Infinity", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: Infinity });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
|
||||
it("falls back to the default timeout when timeoutMs is a string", async () => {
|
||||
makeSuccessfulFetch();
|
||||
const calls = captureTimeoutCalls();
|
||||
const body = { messages: [{ role: "user", content: "hello" }] };
|
||||
|
||||
await compressWithHeadroom(body, { enabled: true, url: "http://localhost:8787", timeoutMs: "5000" });
|
||||
|
||||
expect(calls).toContain(3000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatHeadroomLog", () => {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Multi-transport providers must keep the request body and selected endpoint on
|
||||
* the same wire format. MiniMax-M3 declares a Claude target for compatibility,
|
||||
* but an OpenAI client should use MiniMax's matching OpenAI transport without
|
||||
* an OpenAI -> Claude translation.
|
||||
* Regression: https://github.com/decolua/9router/issues/3418
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
executeMock,
|
||||
translateRequestMock,
|
||||
handleNonStreamingResponseMock,
|
||||
} = vi.hoisted(() => ({
|
||||
executeMock: vi.fn(),
|
||||
translateRequestMock: vi.fn((sourceFormat, targetFormat, model, body) => ({
|
||||
...body,
|
||||
model,
|
||||
_translatedFrom: sourceFormat,
|
||||
_translatedTo: targetFormat,
|
||||
})),
|
||||
handleNonStreamingResponseMock: vi.fn(async () => ({ success: true })),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/executors/index.js", () => ({
|
||||
getExecutor: vi.fn(() => ({
|
||||
execute: executeMock,
|
||||
refreshCredentials: vi.fn().mockResolvedValue(null),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/translator/index.js", () => ({
|
||||
translateRequest: translateRequestMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/handlers/chatCore/nonStreamingHandler.js", () => ({
|
||||
handleNonStreamingResponse: handleNonStreamingResponseMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/requestLogger.js", () => ({
|
||||
createRequestLogger: vi.fn(async () => ({
|
||||
logClientRawRequest: vi.fn(),
|
||||
logRawRequest: vi.fn(),
|
||||
logTargetRequest: vi.fn(),
|
||||
logError: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/clientDetector.js", () => ({
|
||||
detectClientTool: vi.fn(() => null),
|
||||
isNativePassthrough: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/bypassHandler.js", () => ({
|
||||
handleBypassRequest: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/streamHandler.js", () => ({
|
||||
createStreamController: vi.fn(() => ({
|
||||
signal: undefined,
|
||||
handleComplete: vi.fn(),
|
||||
handleError: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/services/tokenRefresh.js", () => ({
|
||||
refreshWithRetry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||
default: vi.fn(),
|
||||
proxyAwareFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/translator/formats/claude.js", () => ({
|
||||
normalizeClaudePassthrough: vi.fn(),
|
||||
anchorClaudeCache: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/toolDeduper.js", () => ({
|
||||
dedupeTools: vi.fn((tools) => ({ tools, stripped: [] })),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/rtk/caveman.js", () => ({ injectCaveman: vi.fn() }));
|
||||
vi.mock("../../open-sse/rtk/ponytail.js", () => ({ injectPonytail: vi.fn() }));
|
||||
vi.mock("../../open-sse/rtk/index.js", () => ({
|
||||
compressMessages: vi.fn(() => null),
|
||||
formatRtkLog: vi.fn(() => ""),
|
||||
}));
|
||||
vi.mock("../../open-sse/rtk/headroom.js", () => ({
|
||||
compressWithHeadroom: vi.fn(async () => null),
|
||||
formatHeadroomLog: vi.fn(() => ""),
|
||||
formatHeadroomSizeLog: vi.fn(() => ""),
|
||||
isHeadroomPhantomSavings: vi.fn(() => false),
|
||||
}));
|
||||
vi.mock("../../open-sse/rtk/pxpipe.js", () => ({
|
||||
compressWithPxpipe: vi.fn(async () => ({ body: null, summary: null })),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/translator/concerns/prefetch.js", () => ({
|
||||
prefetchRemoteImages: vi.fn(async () => 0),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/handlers/chatCore/requestDetail.js", () => ({
|
||||
buildRequestDetail: vi.fn((detail) => detail),
|
||||
extractRequestConfig: vi.fn((body, stream) => ({ body, stream })),
|
||||
}));
|
||||
|
||||
vi.mock("../../open-sse/utils/error.js", () => ({
|
||||
createErrorResult: vi.fn((status, message) => ({ success: false, status, error: message })),
|
||||
formatProviderError: vi.fn((error) => error.message),
|
||||
parseUpstreamError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/usageDb.js", () => ({
|
||||
trackPendingRequest: vi.fn(),
|
||||
appendRequestLog: vi.fn(() => Promise.resolve()),
|
||||
saveRequestDetail: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
function makeOptions(body) {
|
||||
return {
|
||||
body,
|
||||
modelInfo: { provider: "minimax-cn", model: "MiniMax-M3" },
|
||||
credentials: { apiKey: "test-api-key", providerSpecificData: {} },
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/chat/completions",
|
||||
body,
|
||||
headers: { accept: "application/json" },
|
||||
},
|
||||
connectionId: "test-connection",
|
||||
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
};
|
||||
}
|
||||
|
||||
describe("MiniMax-M3 multi-transport routing", () => {
|
||||
beforeEach(() => {
|
||||
executeMock.mockReset();
|
||||
translateRequestMock.mockClear();
|
||||
handleNonStreamingResponseMock.mockClear();
|
||||
executeMock.mockResolvedValue({
|
||||
response: new Response("{}", {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
url: "https://api.minimaxi.com/v1/chat/completions",
|
||||
headers: {},
|
||||
transformedBody: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps OpenAI image blocks on the matching OpenAI transport", async () => {
|
||||
const imageBlock = {
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,AAAB" },
|
||||
};
|
||||
const body = {
|
||||
model: "minimax-cn/MiniMax-M3",
|
||||
stream: false,
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Describe this image" }, imageBlock],
|
||||
}],
|
||||
};
|
||||
|
||||
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.js");
|
||||
await handleChatCore(makeOptions(body));
|
||||
|
||||
expect(translateRequestMock).toHaveBeenCalledWith(
|
||||
"openai",
|
||||
"openai",
|
||||
"MiniMax-M3",
|
||||
expect.any(Object),
|
||||
false,
|
||||
expect.any(Object),
|
||||
"minimax-cn",
|
||||
expect.any(Object),
|
||||
expect.anything(),
|
||||
"test-connection",
|
||||
null,
|
||||
);
|
||||
expect(executeMock).toHaveBeenCalledTimes(1);
|
||||
const requestBody = executeMock.mock.calls[0][0].body;
|
||||
expect(requestBody.messages[0].content).toContainEqual(imageBlock);
|
||||
expect(requestBody._translatedTo).toBe("openai");
|
||||
expect(requestBody).not.toHaveProperty("system");
|
||||
expect(executeMock.mock.calls[0][0].credentials.runtimeTransport.format).toBe("openai");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
import { createSSETransformStreamWithLogger } from "../../open-sse/utils/stream.js";
|
||||
|
||||
// Ollama streams NDJSON — one raw JSON object per line, no "data: " prefix.
|
||||
// Whatever arrives without a closing newline stays in the line buffer and is
|
||||
// only parsed when the transform flushes.
|
||||
async function runOllamaStream(input) {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(input));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const output = stream.pipeThrough(
|
||||
createSSETransformStreamWithLogger(FORMATS.OLLAMA, FORMATS.OPENAI, "ollama", null, null, "gpt-oss:120b"),
|
||||
);
|
||||
|
||||
const reader = output.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
text += decoder.decode(value, { stream: true });
|
||||
}
|
||||
return text + decoder.decode();
|
||||
}
|
||||
|
||||
const chunk = (content, done = false) => JSON.stringify({
|
||||
model: "gpt-oss:120b",
|
||||
created_at: "2026-08-25T00:00:00Z",
|
||||
message: { role: "assistant", content },
|
||||
done,
|
||||
...(done ? { done_reason: "stop", prompt_eval_count: 11, eval_count: 7 } : {}),
|
||||
});
|
||||
|
||||
const deltas = (sse) => sse
|
||||
.split("\n")
|
||||
.filter((l) => l.startsWith("data: ") && l !== "data: [DONE]")
|
||||
.map((l) => JSON.parse(l.slice(6)));
|
||||
|
||||
describe("Ollama NDJSON stream: the tail left in the line buffer", () => {
|
||||
it("delivers a content chunk that arrived without its newline", async () => {
|
||||
const out = await runOllamaStream([chunk("hello"), chunk(" world")].join("\n"));
|
||||
const content = deltas(out).map((c) => c.choices?.[0]?.delta?.content || "").join("");
|
||||
expect(content).toBe("hello world");
|
||||
});
|
||||
|
||||
it("delivers the final chunk — finish_reason and usage — when it arrives without its newline", async () => {
|
||||
const out = await runOllamaStream([chunk("hello"), chunk("", true)].join("\n"));
|
||||
const last = deltas(out).at(-1);
|
||||
expect(last.choices[0].finish_reason).toBe("stop");
|
||||
expect(last.usage).toEqual({ prompt_tokens: 11, completion_tokens: 7, total_tokens: 18 });
|
||||
});
|
||||
|
||||
it("is unchanged when every line is newline-terminated", async () => {
|
||||
const out = await runOllamaStream(`${[chunk("hello"), chunk(" world"), chunk("", true)].join("\n")}\n`);
|
||||
const parsed = deltas(out);
|
||||
expect(parsed.map((c) => c.choices?.[0]?.delta?.content || "").join("")).toBe("hello world");
|
||||
expect(parsed.at(-1).choices[0].finish_reason).toBe("stop");
|
||||
expect(parsed.at(-1).usage.total_tokens).toBe(18);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSE providers keep their sentinel handling", () => {
|
||||
it("does not translate a trailing data: [DONE]", async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(
|
||||
`data: ${JSON.stringify({ choices: [{ delta: { content: "hi" } }] })}\ndata: [DONE]`,
|
||||
));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const out = stream.pipeThrough(
|
||||
createSSETransformStreamWithLogger(FORMATS.OPENAI, FORMATS.OPENAI, "openai", null, null, "gpt-4o"),
|
||||
);
|
||||
const reader = out.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
text += decoder.decode(value, { stream: true });
|
||||
}
|
||||
text += decoder.decode();
|
||||
expect(text).toContain('"content":"hi"');
|
||||
// The sentinel is a framing marker, not a chunk — it must not be translated.
|
||||
expect(text).not.toContain('"done":true');
|
||||
});
|
||||
});
|
||||
@@ -22,8 +22,8 @@ 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.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6",
|
||||
"deepseek-v4-pro", "deepseek-v4-flash",
|
||||
"glm-5.3-flash", "glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6",
|
||||
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp",
|
||||
"mimo-v2.5", "mimo-v2.5-pro",
|
||||
"minimax-m3", "minimax-m2.7", "minimax-m2.5",
|
||||
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
|
||||
import { PROVIDER_MODELS } from "../../open-sse/config/providerModels.js";
|
||||
import { getThinkingLevels } from "../../open-sse/providers/thinkingLevels.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
import { OpenCodeExecutor } from "../../open-sse/executors/opencode.js";
|
||||
import "../translator/registerAll.js";
|
||||
import { translateRequest } from "../../open-sse/translator/index.js";
|
||||
|
||||
const MODEL = "muse-spark-1.2-contributor-free";
|
||||
const PROVIDER = "opencode";
|
||||
|
||||
const input = [{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Think, then answer: 2 + 2?" }],
|
||||
}];
|
||||
|
||||
describe("OpenCode Free Muse Spark thinking", () => {
|
||||
it("advertises reasoning and the requested model limits", () => {
|
||||
expect(PROVIDER_MODELS.oc?.some((model) => model.id === MODEL)).toBe(true);
|
||||
expect(getCapabilitiesForModel(PROVIDER, MODEL)).toMatchObject({
|
||||
reasoning: true,
|
||||
thinkingFormat: "openai",
|
||||
contextWindow: 1048576,
|
||||
maxOutput: 131072,
|
||||
});
|
||||
expect(getCapabilitiesForModel(PROVIDER, `oc/${MODEL}`)).toMatchObject({
|
||||
reasoning: true,
|
||||
contextWindow: 1048576,
|
||||
maxOutput: 131072,
|
||||
});
|
||||
expect(getThinkingLevels(PROVIDER, MODEL)).toEqual([
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
]);
|
||||
});
|
||||
|
||||
it("clamps max to xhigh and emits the Responses reasoning shape", () => {
|
||||
const body = {
|
||||
input,
|
||||
reasoning: { effort: "max" },
|
||||
max_tokens: 131072,
|
||||
};
|
||||
|
||||
const out = new OpenCodeExecutor().transformRequest(MODEL, body, true, {
|
||||
connectionId: "opencode-muse-spark-test",
|
||||
});
|
||||
|
||||
expect(out.reasoning).toEqual({ effort: "xhigh", summary: "auto" });
|
||||
expect(out.reasoning_effort).toBeUndefined();
|
||||
expect(out.max_output_tokens).toBe(131072);
|
||||
expect(out.max_tokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves the other free models on Chat Completions", () => {
|
||||
const executor = new OpenCodeExecutor();
|
||||
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 1024 };
|
||||
executor.transformRequest("big-pickle", body, true, {});
|
||||
expect(executor.buildUrl("big-pickle")).toBe("https://opencode.ai/zen/v1/chat/completions");
|
||||
expect(body.max_tokens).toBe(1024);
|
||||
expect(body.max_output_tokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it("translates Chat Completions max thinking into a Responses request", () => {
|
||||
const body = {
|
||||
model: `oc/${MODEL}`,
|
||||
messages: [{ role: "user", content: "Think, then answer: 2 + 2?" }],
|
||||
reasoning_effort: "max",
|
||||
max_tokens: 131072,
|
||||
};
|
||||
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
MODEL,
|
||||
body,
|
||||
true,
|
||||
{},
|
||||
PROVIDER,
|
||||
);
|
||||
const out = new OpenCodeExecutor().transformRequest(MODEL, translated, true, {
|
||||
connectionId: "opencode-muse-spark-translation-test",
|
||||
});
|
||||
|
||||
expect(out.reasoning).toEqual({ effort: "xhigh", summary: "auto" });
|
||||
expect(out.max_output_tokens).toBe(131072);
|
||||
expect(out.max_tokens).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { injectSystemPrompt } from "../../open-sse/rtk/systemInject.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
import { OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM } from "../../open-sse/translator/schema/blocks.js";
|
||||
import { ROLE } from "../../open-sse/translator/schema/roles.js";
|
||||
import { injectCaveman } from "../../open-sse/rtk/caveman.js";
|
||||
import { injectPonytail } from "../../open-sse/rtk/ponytail.js";
|
||||
import { CAVEMAN_PROMPTS } from "../../open-sse/rtk/cavemanPrompts.js";
|
||||
import { PONYTAIL_PROMPTS } from "../../open-sse/rtk/ponytailPrompt.js";
|
||||
|
||||
const SEP = "\n\n";
|
||||
const P1 = "CAVEMAN_TEST_PROMPT_AAA";
|
||||
const P2 = "PONYTAIL_TEST_PROMPT_BBB";
|
||||
|
||||
describe("system-inject chat messages", () => {
|
||||
it("appends TEXT block to existing system string with SEP", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }, { role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("appends TEXT block to existing system array with OPENAI_BLOCK.TEXT never input_text", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: [{ type: OPENAI_BLOCK.TEXT, text: "hello" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const arr = body.messages[0].content;
|
||||
expect(arr[arr.length - 1]).toEqual({ type: OPENAI_BLOCK.TEXT, text: P1 });
|
||||
expect(arr.some(c => c.type === "input_text")).toBe(false);
|
||||
});
|
||||
|
||||
it("unshifts system message when no system/developer present", () => {
|
||||
const body = { messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0]).toEqual({ role: ROLE.SYSTEM, content: P1 });
|
||||
expect(body.messages[1].role).toBe(ROLE.USER);
|
||||
});
|
||||
|
||||
it("handles developer role as system", () => {
|
||||
const body = { messages: [{ role: ROLE.DEVELOPER, content: "dev" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`dev${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("exact full-prompt idempotency for chat string", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
// different prompt both apply
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("exact full-prompt idempotency for chat array", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: [{ type: OPENAI_BLOCK.TEXT, text: "hello" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const texts = body.messages[0].content.filter(c => c.text === P1);
|
||||
expect(texts.length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.messages[0].content.filter(c => c.text === P2).length).toBe(1);
|
||||
});
|
||||
|
||||
it("never uses first-100 fingerprint: long prompt exact idempotency", () => {
|
||||
const longA = "X".repeat(150) + "_A";
|
||||
const longB = "X".repeat(150) + "_B";
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "base" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longA);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longB);
|
||||
expect(body.messages[0].content).toContain(longA);
|
||||
expect(body.messages[0].content).toContain(longB);
|
||||
// retry same longA is idempotent
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longA);
|
||||
const countA = body.messages[0].content.split(longA).length - 1;
|
||||
expect(countA).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject responses input[]", () => {
|
||||
it("modifies only type: message system/developer and preserves non-message order", () => {
|
||||
const body = {
|
||||
input: [
|
||||
{ type: RESPONSES_ITEM.FUNCTION_CALL, call_id: "c1", name: "fn" },
|
||||
{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "sys" }] },
|
||||
{ type: RESPONSES_ITEM.REASONING, summary: "x" },
|
||||
{ type: RESPONSES_ITEM.FUNCTION_CALL_OUTPUT, call_id: "c1", output: "ok" },
|
||||
],
|
||||
};
|
||||
const before = JSON.parse(JSON.stringify(body.input));
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
// length unchanged except injection inside message
|
||||
expect(body.input.length).toBe(before.length);
|
||||
expect(body.input[0]).toEqual(before[0]);
|
||||
expect(body.input[2]).toEqual(before[2]);
|
||||
expect(body.input[3]).toEqual(before[3]);
|
||||
// system message got INPUT_TEXT appended
|
||||
const sys = body.input[1];
|
||||
expect(sys.content[sys.content.length - 1]).toEqual({ type: RESPONSES_ITEM.INPUT_TEXT, text: P1 });
|
||||
});
|
||||
|
||||
it("appends INPUT_TEXT to array content", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }, { type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "base" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const sys = body.input.find(m => m.role === ROLE.SYSTEM);
|
||||
expect(sys.content[sys.content.length - 1].type).toBe(RESPONSES_ITEM.INPUT_TEXT);
|
||||
expect(sys.content[sys.content.length - 1].text).toBe(P1);
|
||||
});
|
||||
|
||||
it("creates typed message at index 0 if absent preserving order", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }, { type: RESPONSES_ITEM.FUNCTION_CALL, call_id: "1", name: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input[0]).toEqual({ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: P1 }] });
|
||||
expect(body.input[1].role).toBe(ROLE.USER);
|
||||
});
|
||||
|
||||
it("instructions string takes precedence over input[]", () => {
|
||||
const body = { instructions: "instr", input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`instr${SEP}${P1}`);
|
||||
expect(body.input.length).toBe(1);
|
||||
expect(body.input[0].content[0].text).toBe("hi");
|
||||
});
|
||||
|
||||
it("does not coerce string input", () => {
|
||||
const body = { input: "hello string" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input).toBe("hello string");
|
||||
expect(body.instructions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exact idempotency for responses input", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "base" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const sys = body.input[0];
|
||||
expect(sys.content.filter(c => c.text === P1).length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(sys.content.filter(c => c.text === P2).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject instructions", () => {
|
||||
it("appends to instructions string with idempotency", () => {
|
||||
const body = { instructions: "base" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("creates instructions when empty", () => {
|
||||
const body = { instructions: "" };
|
||||
// empty string still taken as string field, should become prompt
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(P1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject dispatch by wire shape", () => {
|
||||
it("messages[] means Chat even when format is openai-responses label", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI_RESPONSES, P1);
|
||||
// should still treat as Chat because messages present
|
||||
expect(body.messages[0].content).toBe(`hi${SEP}${P1}`);
|
||||
});
|
||||
it("input[] means Responses even when format is openai", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input[0].role).toBe(ROLE.SYSTEM);
|
||||
expect(body.input[0].content[0].type).toBe(RESPONSES_ITEM.INPUT_TEXT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject claude", () => {
|
||||
it("string system appends with SEP and idempotent", () => {
|
||||
const body = { system: "base" };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P2);
|
||||
expect(body.system).toBe(`base${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("array system uses CLAUDE_BLOCK.TEXT and inserts before last cache_control", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }, { type: CLAUDE_BLOCK.TEXT, text: "b", cache_control: { type: "ephemeral" } }, { type: CLAUDE_BLOCK.TEXT, text: "c", cache_control: { type: "ephemeral" } }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
// should be inserted before last cache_control (index 2)
|
||||
expect(body.system[2]).toEqual({ type: CLAUDE_BLOCK.TEXT, text: P1 });
|
||||
expect(body.system[3].text).toBe("c");
|
||||
expect(body.system[3].cache_control).toBeDefined();
|
||||
});
|
||||
|
||||
it("array without cache_control appends", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system[body.system.length - 1]).toEqual({ type: CLAUDE_BLOCK.TEXT, text: P1 });
|
||||
});
|
||||
|
||||
it("exact idempotency for claude array", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system.filter(b => b.text === P1).length).toBe(1);
|
||||
});
|
||||
|
||||
it("creates system when absent", () => {
|
||||
const body = {};
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(P1);
|
||||
});
|
||||
|
||||
it("real body with messages[] injects into system, never a system role turn", () => {
|
||||
const body = { system: "base", messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
expect(body.messages).toEqual([{ role: ROLE.USER, content: "hi" }]);
|
||||
});
|
||||
|
||||
it("absent system with messages[] creates system field, not a system message", () => {
|
||||
const body = { messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(P1);
|
||||
expect(body.messages.some(m => m.role === ROLE.SYSTEM)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject gemini", () => {
|
||||
it("preserves snake_case key", () => {
|
||||
const body = { system_instruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.system_instruction.parts.length).toBe(2);
|
||||
expect(body.system_instruction.parts[1].text).toBe(P1);
|
||||
expect(body.systemInstruction).toBeUndefined();
|
||||
});
|
||||
it("preserves camelCase key", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts[1].text).toBe(P1);
|
||||
expect(body.system_instruction).toBeUndefined();
|
||||
});
|
||||
it("handles Antigravity wrapper request.systemInstruction", () => {
|
||||
const body = { request: { systemInstruction: { parts: [{ text: "base" }] } } };
|
||||
injectSystemPrompt(body, FORMATS.ANTIGRAVITY, P1);
|
||||
expect(body.request.systemInstruction.parts[1].text).toBe(P1);
|
||||
});
|
||||
it("exact idempotency for gemini", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts.filter(p => p.text === P1).length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P2);
|
||||
expect(body.systemInstruction.parts.filter(p => p.text === P2).length).toBe(1);
|
||||
});
|
||||
it("creates when absent", () => {
|
||||
const body = {};
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts[0].text).toBe(P1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject kiro", () => {
|
||||
it("updates systemPrompt and mirrored prefix of first history user preserving tail", () => {
|
||||
const oldPrompt = "OLD_SYS";
|
||||
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 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}`);
|
||||
// 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", () => {
|
||||
const body = {
|
||||
systemPrompt: "OLD",
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: "different head 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");
|
||||
});
|
||||
|
||||
it("exact retry idempotency for kiro", () => {
|
||||
const oldPrompt = "OLD";
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: `${oldPrompt}${SEP}tail`, modelId: "m" } }],
|
||||
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
const after1 = JSON.parse(JSON.stringify(body));
|
||||
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
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P2);
|
||||
expect(body.systemPrompt).toBe(`${oldPrompt}${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" } } },
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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 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 }],
|
||||
},
|
||||
};
|
||||
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`);
|
||||
// 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);
|
||||
});
|
||||
|
||||
it("kiro shape gate: stray conversationState without history/currentMessage does not hijack chat body", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }], systemPrompt: "", conversationState: {} };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("substring occurrence does not suppress injection (exact SEP-delimited idempotency)", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "You are RULE follower" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, "RULE");
|
||||
expect(body.messages[0].content).toBe(`You are RULE follower${SEP}RULE`);
|
||||
});
|
||||
|
||||
it("instructions substring occurrence does not suppress injection", () => {
|
||||
const body = { instructions: "You are RULE follower" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, "RULE");
|
||||
expect(body.instructions).toBe(`You are RULE follower${SEP}RULE`);
|
||||
});
|
||||
|
||||
it("kiro empty-old prepend fires when prompt appears mid-tail only", () => {
|
||||
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`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject fail-open", () => {
|
||||
it("null/undefined bodies never throw", () => {
|
||||
expect(() => injectSystemPrompt(null, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt(undefined, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({}, FORMATS.OPENAI, null)).not.toThrow();
|
||||
});
|
||||
|
||||
it("malformed messages array never throws", () => {
|
||||
expect(() => injectSystemPrompt({ messages: null }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({ messages: "bad" }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({ messages: [{ role: null, content: null }] }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen body never throws and does not partially mutate", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }] };
|
||||
Object.freeze(body);
|
||||
Object.freeze(body.messages);
|
||||
Object.freeze(body.messages[0]);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(body.messages[0].content).toBe("hello");
|
||||
});
|
||||
|
||||
it("Proxy throwing setter never throws", () => {
|
||||
const throwingMsg = new Proxy({ role: ROLE.SYSTEM, content: "hello" }, {
|
||||
set() { throw new Error("msg setter fail"); },
|
||||
});
|
||||
const arrProxy = new Proxy([throwingMsg], {
|
||||
get(t, p, r) { return Reflect.get(t, p, r); },
|
||||
set() { throw new Error("arr setter fail"); },
|
||||
});
|
||||
const proxy = new Proxy({}, {
|
||||
set(t, p, v) { if (p === "messages") throw new Error("setter fail"); return Reflect.set(t, p, v); },
|
||||
get(t, p) { if (p === "messages") return arrProxy; return t[p]; },
|
||||
});
|
||||
expect(() => injectSystemPrompt(proxy, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt(proxy, FORMATS.OPENAI_RESPONSES, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen claude never throws", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
Object.freeze(body.system);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.CLAUDE, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen gemini never throws", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "a" }] } };
|
||||
Object.freeze(body.systemInstruction.parts);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.GEMINI, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("injectCaveman and injectPonytail fail open on frozen", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hi" }] };
|
||||
Object.freeze(body);
|
||||
Object.freeze(body.messages);
|
||||
expect(() => injectCaveman(body, FORMATS.OPENAI, "full")).not.toThrow();
|
||||
expect(() => injectPonytail(body, FORMATS.OPENAI, "full")).not.toThrow();
|
||||
});
|
||||
|
||||
it("different caveman and ponytail prompts both apply", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "base" }] };
|
||||
injectCaveman(body, FORMATS.OPENAI, "full");
|
||||
const afterCaveman = body.messages[0].content;
|
||||
expect(afterCaveman).toContain(CAVEMAN_PROMPTS.full.slice(0, 30));
|
||||
injectPonytail(body, FORMATS.OPENAI, "full");
|
||||
expect(body.messages[0].content).toContain(PONYTAIL_PROMPTS.full.slice(0, 30));
|
||||
expect(body.messages[0].content).toContain(afterCaveman);
|
||||
});
|
||||
});
|
||||
@@ -102,19 +102,38 @@ describe("refreshAccessToken — config-driven profiles", () => {
|
||||
expect(fm).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshAccessToken — legacy generic path (no profile)", () => {
|
||||
describe("Cline refresh", () => {
|
||||
beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; });
|
||||
afterEach(() => { global.fetch = originalFetch; });
|
||||
|
||||
it("still works for an unprofiled provider via config.refreshUrl/clientId/clientSecret", async () => {
|
||||
const fm = mockFetchOnce({ access_token: "gen-acc", expires_in: 3600 });
|
||||
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
|
||||
it("uses the extension JSON refresh contract", async () => {
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
const fm = mockFetchOnce({
|
||||
data: {
|
||||
accessToken: "cline-acc",
|
||||
refreshToken: "cline-rot",
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
const { refreshTokenByProvider } = await import(
|
||||
"open-sse/services/tokenRefresh.js"
|
||||
);
|
||||
|
||||
await refreshAccessToken("cline", "gen-old", {}, console);
|
||||
const out = await refreshTokenByProvider(
|
||||
"cline",
|
||||
{ refreshToken: "cline-old" },
|
||||
console
|
||||
);
|
||||
|
||||
const body = new URLSearchParams(fm.mock.calls[0][1].body);
|
||||
expect(body.get("grant_type")).toBe("refresh_token");
|
||||
expect(body.get("client_id")).toBeTruthy();
|
||||
const [, init] = fm.mock.calls[0];
|
||||
expect(init.headers["Content-Type"]).toBe("application/json");
|
||||
expect(JSON.parse(init.body)).toEqual({
|
||||
refreshToken: "cline-old",
|
||||
grantType: "refresh_token",
|
||||
clientType: "extension",
|
||||
});
|
||||
expect(out.accessToken).toBe("cline-acc");
|
||||
expect(out.refreshToken).toBe("cline-rot");
|
||||
expect(out.expiresIn).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ const SUPPORTED = [
|
||||
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
|
||||
"qoder", "iflow", "ollama", "glm", "glm-cn",
|
||||
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi",
|
||||
"deepseek",
|
||||
"deepseek", "zed",
|
||||
];
|
||||
|
||||
describe("usage dispatch", () => {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import REGISTRY from "../../open-sse/providers/registry/index.js";
|
||||
import { buildSearchRequest } from "../../open-sse/handlers/search/callers.js";
|
||||
import { handleSearchCore } from "../../open-sse/handlers/search/index.js";
|
||||
import { normalizeSearchResponse } from "../../open-sse/handlers/search/normalizers.js";
|
||||
import { AI_PROVIDERS, getProvidersByKind } from "@/shared/constants/providers.js";
|
||||
|
||||
const CONFIG = {
|
||||
id: "xquik",
|
||||
baseUrl: "https://xquik.com/api/v1/x/tweets/search",
|
||||
method: "GET",
|
||||
authType: "apikey",
|
||||
searchTypes: ["x"],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 100,
|
||||
creditsPerResult: 1,
|
||||
};
|
||||
|
||||
const PARAMS = {
|
||||
query: "from:github release notes",
|
||||
searchType: "x",
|
||||
maxResults: 10,
|
||||
token: "xq_test_key",
|
||||
language: "en",
|
||||
providerOptions: { queryType: "Latest", cursor: "next page" },
|
||||
};
|
||||
|
||||
const RESPONSE = {
|
||||
tweets: [
|
||||
{
|
||||
id: "1234567890",
|
||||
text: "Release notes are live.",
|
||||
createdAt: "2026-08-25T12:00:00Z",
|
||||
author: { username: "github", name: "GitHub" },
|
||||
media: [{ mediaUrl: "https://pbs.twimg.com/media/example.jpg", type: "photo" }],
|
||||
},
|
||||
],
|
||||
has_next_page: true,
|
||||
next_cursor: "cursor-2",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("Xquik search provider", () => {
|
||||
it("registers a dedicated X search provider with no-charge key validation", () => {
|
||||
const entry = REGISTRY.find((candidate) => candidate.id === "xquik");
|
||||
|
||||
expect(entry).toMatchObject({
|
||||
category: "apikey",
|
||||
serviceKinds: ["webSearch"],
|
||||
searchConfig: {
|
||||
authHeader: "x-api-key",
|
||||
validateUrl: "https://xquik.com/api/v1/credits",
|
||||
searchTypes: ["x"],
|
||||
creditsPerResult: 1,
|
||||
},
|
||||
});
|
||||
expect(AI_PROVIDERS.xquik?.searchConfig).toEqual(entry.searchConfig);
|
||||
expect(getProvidersByKind("webSearch").map((provider) => provider.id)).toContain("xquik");
|
||||
});
|
||||
|
||||
it("builds the documented GET request without putting the key in the URL", () => {
|
||||
const request = buildSearchRequest(CONFIG, PARAMS);
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(url.origin + url.pathname).toBe("https://xquik.com/api/v1/x/tweets/search");
|
||||
expect(Object.fromEntries(url.searchParams)).toEqual({
|
||||
q: "from:github release notes",
|
||||
limit: "10",
|
||||
cursor: "next page",
|
||||
queryType: "Latest",
|
||||
language: "en",
|
||||
});
|
||||
expect(url.search).not.toContain("xq_test_key");
|
||||
expect(request.init).toEqual({
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json", "x-api-key": "xq_test_key" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unsupported query types before contacting Xquik", () => {
|
||||
expect(() => buildSearchRequest(CONFIG, {
|
||||
...PARAMS,
|
||||
providerOptions: { queryType: "Popular" },
|
||||
})).toThrow("Xquik queryType must be Latest or Top");
|
||||
});
|
||||
|
||||
it("normalizes posts and preserves cursor pagination", () => {
|
||||
const normalized = normalizeSearchResponse("xquik", RESPONSE, PARAMS.query, "x");
|
||||
|
||||
expect(normalized.totalResults).toBeNull();
|
||||
expect(normalized.pagination).toEqual({ has_more: true, next_cursor: "cursor-2" });
|
||||
expect(normalized.results).toHaveLength(1);
|
||||
expect(normalized.results[0]).toMatchObject({
|
||||
title: "@github on X",
|
||||
url: "https://x.com/github/status/1234567890",
|
||||
display_url: "x.com/github/status/1234567890",
|
||||
snippet: "Release notes are live.",
|
||||
published_at: "2026-08-25T12:00:00Z",
|
||||
metadata: {
|
||||
author: "@github",
|
||||
source_type: "x_post",
|
||||
image_url: "https://pbs.twimg.com/media/example.jpg",
|
||||
},
|
||||
citation: { provider: "xquik", rank: 1 },
|
||||
});
|
||||
expect(normalized.results[0].content).toEqual({
|
||||
format: "text",
|
||||
text: "Release notes are live.",
|
||||
length: 23,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the stable status URL when author data is unavailable", () => {
|
||||
const normalized = normalizeSearchResponse("xquik", {
|
||||
tweets: [{ id: "9876543210", text: "Author data is unavailable." }],
|
||||
has_next_page: false,
|
||||
next_cursor: "",
|
||||
}, PARAMS.query, "x");
|
||||
|
||||
expect(normalized.results[0]).toMatchObject({
|
||||
title: "X post",
|
||||
url: "https://x.com/i/web/status/9876543210",
|
||||
metadata: { author: null, source_type: "x_post" },
|
||||
});
|
||||
expect(normalized.pagination).toEqual({ has_more: false, next_cursor: null });
|
||||
});
|
||||
|
||||
it("reports Xquik credits without claiming an unknown USD cost", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify(RESPONSE), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})));
|
||||
|
||||
const result = await handleSearchCore({
|
||||
body: { query: PARAMS.query, max_results: 10, provider_options: PARAMS.providerOptions },
|
||||
provider: { id: "xquik" },
|
||||
providerConfig: CONFIG,
|
||||
credentials: { apiKey: "xq_test_key" },
|
||||
});
|
||||
const payload = await result.response.json();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(payload.usage).toEqual({
|
||||
queries_used: 1,
|
||||
search_cost_usd: null,
|
||||
provider_credits_used: 1,
|
||||
});
|
||||
expect(payload.pagination).toEqual({ has_more: true, next_cursor: "cursor-2" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../open-sse/shared/zedAuth.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
fetchZedAuthenticatedUser: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { fetchZedAuthenticatedUser } from "../../open-sse/shared/zedAuth.js";
|
||||
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.js";
|
||||
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||
import {
|
||||
formatZedPlanLabel,
|
||||
parseZedUsageLimit,
|
||||
parseZedAuthenticatedUserUsage,
|
||||
} from "../../open-sse/services/usage/zed.js";
|
||||
|
||||
describe("zed registry usage flags", () => {
|
||||
it("is listed in USAGE_SUPPORTED_PROVIDERS", () => {
|
||||
expect(USAGE_SUPPORTED_PROVIDERS).toContain("zed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseZedUsageLimit", () => {
|
||||
it("parses unlimited string and object forms", () => {
|
||||
expect(parseZedUsageLimit("unlimited")).toEqual({ unlimited: true, total: 0 });
|
||||
expect(parseZedUsageLimit({ unlimited: true })).toEqual({ unlimited: true, total: 0 });
|
||||
});
|
||||
|
||||
it("parses numeric and limited object forms", () => {
|
||||
expect(parseZedUsageLimit(50)).toEqual({ unlimited: false, total: 50 });
|
||||
expect(parseZedUsageLimit("25")).toEqual({ unlimited: false, total: 25 });
|
||||
expect(parseZedUsageLimit({ limited: 40 })).toEqual({ unlimited: false, total: 40 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatZedPlanLabel", () => {
|
||||
it("maps known plan ids", () => {
|
||||
expect(formatZedPlanLabel("zed_pro")).toBe("Zed Pro");
|
||||
expect(formatZedPlanLabel("zed_pro_trial")).toBe("Zed Pro Trial");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseZedAuthenticatedUserUsage", () => {
|
||||
it("maps edit_predictions and billing cycle reset", () => {
|
||||
const parsed = parseZedAuthenticatedUserUsage({
|
||||
plan: {
|
||||
plan_v3: "zed_pro",
|
||||
subscription_period: {
|
||||
started_at: "2026-07-01T00:00:00Z",
|
||||
ended_at: "2026-08-01T00:00:00Z",
|
||||
},
|
||||
usage: {
|
||||
edit_predictions: { used: 12, limit: 50 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.plan).toBe("Zed Pro");
|
||||
expect(parsed.quotas["Edit Predictions"]).toMatchObject({
|
||||
used: 12,
|
||||
total: 50,
|
||||
remainingPercentage: 76,
|
||||
resetAt: "2026-08-01T00:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks unlimited edit predictions at 100% remaining", () => {
|
||||
const parsed = parseZedAuthenticatedUserUsage({
|
||||
plan: {
|
||||
plan_v3: "zed_pro",
|
||||
usage: {
|
||||
edit_predictions: { used: 999, limit: "unlimited" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.quotas["Edit Predictions"]).toMatchObject({
|
||||
used: 999,
|
||||
total: 0,
|
||||
remainingPercentage: 100,
|
||||
unlimited: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips token-billed model_requests limit=0 and adds billing note", () => {
|
||||
const parsed = parseZedAuthenticatedUserUsage({
|
||||
plan: {
|
||||
plan_v3: "zed_student",
|
||||
usage: {
|
||||
model_requests: { used: 0, limit: { limited: 0 } },
|
||||
edit_predictions: { used: 0, limit: "unlimited" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.quotas["Hosted Model Requests"]).toBeUndefined();
|
||||
expect(parsed.quotas["Edit Predictions"]).toBeDefined();
|
||||
expect(parsed.message).toMatch(/token/i);
|
||||
expect(parsed.message).toMatch(/dashboard\.zed\.dev/);
|
||||
});
|
||||
|
||||
it("surfaces overdue invoice warning", () => {
|
||||
const parsed = parseZedAuthenticatedUserUsage({
|
||||
plan: {
|
||||
plan_v3: "zed_pro",
|
||||
has_overdue_invoices: true,
|
||||
usage: {
|
||||
edit_predictions: { used: 0, limit: "unlimited" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.hasOverdueInvoices).toBe(true);
|
||||
expect(parsed.message).toMatch(/overdue invoices/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUsageForProvider(zed)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns quotas from /client/users/me", async () => {
|
||||
fetchZedAuthenticatedUser.mockResolvedValueOnce({
|
||||
plan: {
|
||||
plan_v3: "zed_student",
|
||||
usage: {
|
||||
edit_predictions: { used: 3, limit: 30 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "zed",
|
||||
accessToken: "plain-token",
|
||||
providerSpecificData: { userId: "user-42", systemId: "sys-1" },
|
||||
});
|
||||
|
||||
expect(usage.plan).toBe("Zed Student");
|
||||
expect(usage.quotas["Edit Predictions"]).toMatchObject({
|
||||
used: 3,
|
||||
total: 30,
|
||||
remainingPercentage: 90,
|
||||
});
|
||||
|
||||
expect(fetchZedAuthenticatedUser).toHaveBeenCalledWith(
|
||||
{
|
||||
accessToken: "plain-token",
|
||||
providerSpecificData: { userId: "user-42", systemId: "sys-1" },
|
||||
},
|
||||
{ proxyOptions: null },
|
||||
);
|
||||
});
|
||||
|
||||
it("requires user id on the connection", async () => {
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "zed",
|
||||
accessToken: "plain-token",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/missing user id/i);
|
||||
expect(fetchZedAuthenticatedUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseQuotaData(zed)", () => {
|
||||
it("normalizes zed quotas for QuotaTable", () => {
|
||||
const data = parseZedAuthenticatedUserUsage({
|
||||
plan: {
|
||||
plan_v3: "zed_pro",
|
||||
usage: { edit_predictions: { used: 10, limit: 20 } },
|
||||
},
|
||||
});
|
||||
|
||||
const rows = parseQuotaData("zed", data);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
name: "Edit Predictions",
|
||||
used: 10,
|
||||
total: 20,
|
||||
remainingPercentage: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user