Merge remote-tracking branch 'upstream/master'

This commit is contained in:
MUH. IQRAM BAHRING
2026-09-05 23:20:53 +08:00
97 changed files with 4244 additions and 571 deletions
@@ -0,0 +1,89 @@
import { createRequire } from "module";
import { describe, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const {
ANTIGRAVITY_IDE_VERSION,
applyAntigravityIdeVersionOverride,
} = require("../../src/mitm/antigravityIdeVersion.js");
const CURRENT_VERSION = "2.11.0";
function makeRequest(metadata = { ideName: "antigravity", ideVersion: CURRENT_VERSION }) {
const bodyBuffer = Buffer.from(JSON.stringify({ metadata, request: { contents: [] } }));
const headers = {
"content-type": "application/json",
"content-length": String(bodyBuffer.length),
"user-agent": `antigravity/${CURRENT_VERSION}`,
};
return { bodyBuffer, headers };
}
describe("Antigravity IDE version override", () => {
it("preserves catalog request identity and bytes", () => {
const { bodyBuffer, headers } = makeRequest();
const result = applyAntigravityIdeVersionOverride(
bodyBuffer,
headers,
"/v1internal:fetchAvailableModels"
);
expect(result.applied).toBe(false);
expect(result.bodyBuffer).toBe(bodyBuffer);
expect(result.headers).toBe(headers);
expect(result.headers["user-agent"]).toBe(`antigravity/${CURRENT_VERSION}`);
expect(JSON.parse(result.bodyBuffer.toString()).metadata.ideVersion).toBe(CURRENT_VERSION);
expect(result.headers["content-length"]).toBe(String(bodyBuffer.length));
});
it.each([":generateContent", ":streamGenerateContent"])(
"rewrites identity for %s requests",
(endpoint) => {
const { bodyBuffer, headers } = makeRequest();
const result = applyAntigravityIdeVersionOverride(
bodyBuffer,
headers,
`/v1internal/models/gemini-3.7-flash-tiered${endpoint}`
);
expect(result.applied).toBe(true);
expect(result.bodyBuffer).not.toBe(bodyBuffer);
expect(result.headers["user-agent"]).toBe(`antigravity/${ANTIGRAVITY_IDE_VERSION}`);
expect(JSON.parse(result.bodyBuffer.toString()).metadata.ideVersion).toBe(ANTIGRAVITY_IDE_VERSION);
}
);
it("preserves malformed non-generation request content byte-for-byte", () => {
const bodyBuffer = Buffer.from([0xff, 0x00, 0x7b, 0x6e, 0x6f, 0x74, 0x2d, 0x6a, 0x73, 0x6f, 0x6e]);
const headers = {
"content-type": "application/octet-stream",
"content-length": String(bodyBuffer.length),
"user-agent": `antigravity/${CURRENT_VERSION}`,
};
const result = applyAntigravityIdeVersionOverride(bodyBuffer, headers, "/v1internal:loadCodeAssist");
expect(result.applied).toBe(false);
expect(result.bodyBuffer).toBe(bodyBuffer);
expect(result.headers).toBe(headers);
});
it("does not synthesize missing Antigravity identity", () => {
const bodyBuffer = Buffer.from(JSON.stringify({ metadata: {}, request: { contents: [] } }));
const headers = { "content-type": "application/json", "content-length": String(bodyBuffer.length) };
const result = applyAntigravityIdeVersionOverride(
bodyBuffer,
headers,
"/v1internal/models/gemini-3.7-flash-tiered:generateContent"
);
expect(result.applied).toBe(false);
expect(result.bodyBuffer).toBe(bodyBuffer);
expect(result.headers).toEqual(headers);
expect(result.headers).not.toHaveProperty("user-agent");
expect(JSON.parse(result.bodyBuffer.toString()).metadata).not.toHaveProperty("ideVersion");
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
const proxyAwareFetch = vi.fn(async (url) => ({
ok: true,
status: 200,
json: async () => url.includes(":loadCodeAssist")
? { cloudaicompanionProject: "project-1", currentTier: { name: "Pro" } }
: {
models: {
"gemini-3.8-flash-high": {
displayName: "Gemini 3.8 Flash (High)",
quotaInfo: { remainingFraction: 0.85, resetTime: "2026-08-25T12:00:00Z" },
},
"gemini-3.8-flash-medium": {
displayName: "Gemini 3.8 Flash (Medium)",
quotaInfo: { remainingFraction: 0.6, resetTime: "2026-08-25T12:00:00Z" },
},
"gemini-3.8-flash-low": {
displayName: "Gemini 3.8 Flash (Low)",
quotaInfo: { remainingFraction: 0.35, resetTime: "2026-08-25T12:00:00Z" },
},
"internal-model": {
displayName: "Internal",
isInternal: true,
quotaInfo: { remainingFraction: 0.5 },
},
},
},
text: async () => "{}",
}));
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch,
}));
describe("Antigravity quota tracker: Gemini 3.8 Flash usage bars", () => {
beforeEach(() => proxyAwareFetch.mockClear());
it("returns Gemini 3.8 Flash tier quotas so the dashboard can render usage bars", async () => {
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
const usage = await getAntigravityUsage("access-token", {});
expect(usage.quotas["gemini-3.8-flash-high"]).toMatchObject({
used: 150,
total: 1000,
remainingPercentage: 85,
displayName: "Gemini 3.8 Flash (High)",
});
expect(usage.quotas["gemini-3.8-flash-medium"]).toMatchObject({
used: 400,
total: 1000,
remainingPercentage: 60,
});
expect(usage.quotas["gemini-3.8-flash-low"]).toMatchObject({
used: 650,
total: 1000,
remainingPercentage: 35,
});
});
});
+140 -1
View File
@@ -27,7 +27,7 @@ vi.mock("open-sse/services/usage/google.js", () => ({
}));
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 { getAntigravityQuotaCache, handleAntigravityQuotaError, refreshAntigravityQuota, clearAntigravityStrikes } = await import("@/sse/services/antigravityQuota.js");
const { getProviderCredentials } = await import("@/sse/services/auth.js");
const MODEL = "claude-opus-4-6-thinking";
@@ -169,4 +169,143 @@ describe("Antigravity quota-aware routing", () => {
vi.useRealTimers();
}
});
it("strike-breaks after 3 optimistic 429s within 60s and cache-blocks 15 minutes", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
// Quota API lies: reports 90% remaining while generation keeps 429ing.
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
try {
const first = await handleAntigravityQuotaError("ag-strike", 429, MODEL, "token", {});
expect(first).toBeNull();
const second = await handleAntigravityQuotaError("ag-strike", 429, MODEL, "token", {});
expect(second).toBeNull();
const third = await handleAntigravityQuotaError("ag-strike", 429, MODEL, "token", {});
expect(third).toBe(Date.parse("2026-08-26T00:15:00.000Z"));
} finally {
vi.useRealTimers();
}
});
it("resets the strike counter when strikes fall outside the 60s window", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
try {
await handleAntigravityQuotaError("ag-window", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-window", 429, MODEL, "token", {});
await vi.advanceTimersByTimeAsync(61_000);
const result = await handleAntigravityQuotaError("ag-window", 429, MODEL, "token", {});
expect(result).toBeNull(); // window lapsed — counter restarted at 1
} finally {
vi.useRealTimers();
}
});
it("strike-breaks when the quota API is unavailable (null reading) too", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
// Quota endpoint failing/forbidden => quota unknown. Strikes must still count.
mocks.getAntigravityUsage.mockResolvedValue({ message: "forbidden", quotas: {} });
try {
await handleAntigravityQuotaError("ag-null", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-null", 429, MODEL, "token", {});
const third = await handleAntigravityQuotaError("ag-null", 429, MODEL, "token", {});
expect(third).toBe(Date.parse("2026-08-26T00:15:00.000Z"));
} finally {
vi.useRealTimers();
}
});
it("persists the block into the shared cache so the next request skips the pair", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
try {
await handleAntigravityQuotaError("ag-persist", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-persist", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-persist", 429, MODEL, "token", {});
// The synthesized entry must be visible to the auth pre-filter reading
// the shared cache — and must survive an optimistic upstream refresh.
const cached = getAntigravityQuotaCache().get("ag-persist")?.[MODEL];
expect(cached).toMatchObject({ remainingPercentage: 0 });
expect(Date.parse(cached.resetAt)).toBe(Date.parse("2026-08-26T00:15:00.000Z"));
await refreshAntigravityQuota("ag-persist", "token", {});
expect(getAntigravityQuotaCache().get("ag-persist")?.[MODEL]).toMatchObject({
remainingPercentage: 0,
});
} finally {
vi.useRealTimers();
}
});
it("clears strike state and the synthesized block after a successful request", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
try {
await handleAntigravityQuotaError("ag-clear", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-clear", 429, MODEL, "token", {});
await handleAntigravityQuotaError("ag-clear", 429, MODEL, "token", {});
expect(getAntigravityQuotaCache().get("ag-clear")?.[MODEL]?.remainingPercentage).toBe(0);
clearAntigravityStrikes("ag-clear", MODEL);
// Synthesized entry gone — pair selectable again immediately.
expect(getAntigravityQuotaCache().get("ag-clear")?.[MODEL]).toBeUndefined();
// Two more 429s do NOT inherit earlier strikes: no block on the third-in-episode.
await handleAntigravityQuotaError("ag-clear", 429, MODEL, "token", {});
await expect(handleAntigravityQuotaError("ag-clear", 429, MODEL, "token", {})).resolves.toBeNull();
} finally {
vi.useRealTimers();
}
});
it("anchors the window at the first strike: 3 strikes spread over 90s do not trip", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-26T00:00:00.000Z"));
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
try {
await handleAntigravityQuotaError("ag-anchor", 429, MODEL, "token", {}); // t=0
await vi.advanceTimersByTimeAsync(45_000);
await handleAntigravityQuotaError("ag-anchor", 429, MODEL, "token", {}); // t=45s
await vi.advanceTimersByTimeAsync(45_000);
// t=90s: within 60s of strike #2 but outside 60s of strike #1 => new window
const result = await handleAntigravityQuotaError("ag-anchor", 429, MODEL, "token", {});
expect(result).toBeNull();
} finally {
vi.useRealTimers();
}
});
it("keeps the optimistic path null without touching the quota cache", async () => {
mocks.getAntigravityUsage.mockResolvedValue({ quotas: {
[MODEL]: { remainingPercentage: 90, resetAt: FUTURE_RESET },
} });
await expect(handleAntigravityQuotaError("ag-optimistic", 429, MODEL, "token", {}))
.resolves.toBeNull();
// Optimistic reading must NOT poison the shared cache (auth pre-filter
// treats cached 0% as exhausted).
expect(getAntigravityQuotaCache().get("ag-optimistic")?.[MODEL]?.remainingPercentage).toBe(90);
});
});
+2 -2
View File
@@ -69,13 +69,13 @@ describe("antigravity computeRetryDelay hook (D3)", () => {
it("registry uses the daily IDE cloudcode host and user agent", () => {
expect(antigravity.transport.baseUrls).toEqual(["https://daily-cloudcode-pa.googleapis.com"]);
expect(antigravity.transport.headers["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
expect(antigravity.transport.headers["User-Agent"]).toBe("antigravity/ide/2.11.0 darwin/arm64");
});
it("buildHeaders matches official IDE stream headers", () => {
ag._lastSessionId = "sess-123";
const h = ag.buildHeaders({ accessToken: "tok" }, true);
expect(h["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
expect(h["User-Agent"]).toBe("antigravity/ide/2.11.0 darwin/arm64");
expect(h["Content-Type"]).toBe("application/json");
expect(h["Authorization"]).toBe("Bearer tok");
expect(h).not.toHaveProperty("X-Machine-Session-Id");
+1 -1
View File
@@ -23,7 +23,7 @@ describe("Antigravity usage headers", () => {
expect(proxyAwareFetch).toHaveBeenCalledTimes(2);
for (const [, options] of proxyAwareFetch.mock.calls) {
expect(options.headers["User-Agent"]).toBe("antigravity/ide/2.1.1 darwin/arm64");
expect(options.headers["User-Agent"]).toBe("antigravity/ide/2.11.0 darwin/arm64");
expect(options.headers).not.toHaveProperty("x-request-source");
}
});
+7
View File
@@ -32,6 +32,13 @@ describe("getCapabilitiesForModel", () => {
}
});
it("reports Claude Fable 5.1 as a permanent adaptive-thinking model", () => {
expect(getCapabilitiesForModel("claude", "claude-fable-5-1")).toMatchObject({
...claudeSonnet5Expected,
thinkingCanDisable: false,
});
});
it("reports Kiro Claude Opus 4.8 as a 1M context model", () => {
expect(getCapabilitiesForModel("kiro", "claude-opus-4.8").contextWindow).toBe(1000000);
expect(getCapabilitiesForModel("kiro", "anthropic/claude-opus-4.8").contextWindow).toBe(1000000);
+6 -1
View File
@@ -7,9 +7,14 @@
*/
import { describe, it, expect } from "vitest";
import { cloakClaudeTools, decloakStreamChunk } from "../../open-sse/utils/claudeCloaking.js";
import { applyCloaking, cloakClaudeTools, decloakStreamChunk } from "../../open-sse/utils/claudeCloaking.js";
import { CLAUDE_TOOL_SUFFIX } from "../../open-sse/config/appConstants.js";
it("advertises a Claude Code version accepted by Fable 5.1", () => {
const body = applyCloaking({ messages: [] }, "sk-ant-oat-test", "session-id");
expect(body.system[0].text).toMatch(/^x-anthropic-billing-header: cc_version=2.1.258\./);
});
describe("cloakClaudeTools", () => {
const baseBody = {
tools: [{ name: "todo_write", description: "write todos", input_schema: { type: "object", properties: {} } }],
@@ -0,0 +1,88 @@
// A combo that mixes providers leaks foreign block shapes into the Claude history.
// Anthropic validates server_tool_use ids against ^srvtoolu_[a-zA-Z0-9_]+$ and 400s
// the whole request when a provider (e.g. z.ai/glm) emits OpenAI-style call_ ids.
import { describe, it, expect } from "vitest";
import { normalizeClaudePassthrough } from "../../open-sse/translator/formats/claude.js";
const glmServerToolUse = () => ({
role: "assistant",
content: [
{ type: "text", text: "searching" },
{ type: "server_tool_use", id: "call_50b82aba1b754d82a4408a53", name: "analyze_image", input: {} },
],
});
describe("normalizeClaudePassthrough — foreign server_tool_use ids", () => {
it("drops a server_tool_use block whose id is not an srvtoolu_ id", () => {
const out = normalizeClaudePassthrough({ messages: [glmServerToolUse()] });
expect(out.messages[0].content).toEqual([{ type: "text", text: "searching" }]);
});
it("drops the paired tool_result so no orphan reference is left behind", () => {
const out = normalizeClaudePassthrough({
messages: [
glmServerToolUse(),
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: "call_50b82aba1b754d82a4408a53", content: "boom" },
{ type: "text", text: "keep me" },
],
},
],
});
expect(out.messages[1].content).toEqual([{ type: "text", text: "keep me" }]);
});
it("keeps a well-formed Anthropic server_tool_use block", () => {
const block = { type: "server_tool_use", id: "srvtoolu_01EUi6RNgHntbStfCjgLyLzz", name: "web_search", input: {} };
const out = normalizeClaudePassthrough({ messages: [{ role: "assistant", content: [block] }] });
expect(out.messages[0].content).toEqual([block]);
});
it("keeps regular tool_use blocks, whatever their id looks like", () => {
const block = { type: "tool_use", id: "call_942248714fef4a9abb8e8eff", name: "Bash", input: { command: "ls" } };
const out = normalizeClaudePassthrough({ messages: [{ role: "assistant", content: [block] }] });
expect(out.messages[0].content).toEqual([block]);
});
it("drops a message whose blocks were all stripped instead of padding it with empty text", () => {
const out = normalizeClaudePassthrough({
messages: [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{ role: "assistant", content: [{ type: "server_tool_use", id: "call_x", name: "analyze_image", input: {} }] },
{ role: "user", content: [{ type: "text", text: "bye" }] },
],
});
expect(out.messages).toHaveLength(2);
expect(out.messages.map(m => m.role)).toEqual(["user", "user"]);
});
it("strips empty text blocks a client put in the history (Anthropic 400s them)", () => {
const out = normalizeClaudePassthrough({
messages: [{ role: "assistant", content: [{ type: "text", text: "real" }, { type: "text", text: "" }] }],
});
expect(out.messages[0].content).toEqual([{ type: "text", text: "real" }]);
});
it("drops a message whose content is a single empty text block", () => {
const out = normalizeClaudePassthrough({
messages: [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{ role: "assistant", content: [{ type: "text", text: "" }] },
],
});
expect(out.messages).toHaveLength(1);
});
it("drops a message whose string content is empty", () => {
const out = normalizeClaudePassthrough({
messages: [
{ role: "user", content: "hello" },
{ role: "assistant", content: "" },
],
});
expect(out.messages).toHaveLength(1);
expect(out.messages[0].content).toBe("hello");
});
});
@@ -29,6 +29,7 @@ describe("DefaultExecutor.buildHeaders() — claude provider", () => {
headers["Anthropic-Version"] === "2023-06-01" ||
headers["anthropic-version"] === "2023-06-01";
expect(hasVersion).toBe(true);
expect(headers["User-Agent"]).toBe("claude-cli/2.1.258 (external, sdk-cli)");
});
it("includes heavy-agent beta flags for claude-opus-5", () => {
+19
View File
@@ -125,6 +125,25 @@ describe("dashboard guard public LLM API access", () => {
expect(response.body.error).toBe("API key required for remote API access");
});
it("rejects remote /responses rewrite without API key", async () => {
const response = await proxy(request("/responses", { host: "router.example.com" }));
expect(response.status).toBe(401);
expect(response.body.error).toBe("API key required for remote API access");
});
it("allows remote /responses rewrite with a valid API key", async () => {
mocks.validateApiKey.mockResolvedValue(true);
const response = await proxy(request("/responses", {
host: "router.example.com",
authorization: "Bearer sk-valid",
}));
expect(response).toBe(mocks.nextResponse);
expect(mocks.validateApiKey).toHaveBeenCalledWith("sk-valid");
});
it("allows remote codex rewrite with valid API key", async () => {
mocks.validateApiKey.mockResolvedValue(true);
@@ -0,0 +1,82 @@
/**
* Regression: Anthropic rejects a tool that carries BOTH `defer_loading: true`
* and `cache_control`:
*
* [400] Tool 'mcp__x__y' cannot both defer_loading=true cache_control set.
* Tools defer_loading cannot use prompt caching.
*
* 9router anchors the 1h cache breakpoint on the LAST tool of the array with
* no guard. Clients that speak MCP (Claude Code) put deferred tools at the
* tail, so the anchor lands exactly on a tool that cannot be cached and the
* request 400s before combo fallback can try the next hop.
*
* The fix anchors on the last tool that is NOT deferred, so prompt caching is
* kept for the tools that can use it instead of being dropped wholesale.
*
* See: #3567.
*/
import { describe, it, expect } from "vitest";
import { anchorClaudeCache } from "../../open-sse/translator/formats/claude.js";
import { prepareClaudeRequest } from "../../open-sse/translator/formats/claude.js";
const tool = (name, extra = {}) => ({
name,
description: "t",
input_schema: { type: "object", properties: {} },
...extra,
});
describe("defer_loading tools never carry cache_control (#3567)", () => {
it("anchorClaudeCache: anchor moves to the last non-deferred tool", () => {
const body = anchorClaudeCache({
messages: [{ role: "user", content: "hi" }],
tools: [tool("a"), tool("b"), tool("mcp__x__y", { defer_loading: true })],
});
expect(body.tools[2].cache_control).toBeUndefined();
expect(body.tools[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
expect(body.tools[0].cache_control).toBeUndefined();
});
it("anchorClaudeCache: no tool is cached when every tool is deferred", () => {
const body = anchorClaudeCache({
messages: [{ role: "user", content: "hi" }],
tools: [tool("mcp__a", { defer_loading: true }), tool("mcp__b", { defer_loading: true })],
});
expect(body.tools.every(t => t.cache_control === undefined)).toBe(true);
});
it("anchorClaudeCache: strips a cache_control the client put on a deferred tool", () => {
const body = anchorClaudeCache({
messages: [{ role: "user", content: "hi" }],
tools: [tool("mcp__a", { defer_loading: true, cache_control: { type: "ephemeral" } })],
});
expect(body.tools[0].cache_control).toBeUndefined();
});
it("anchorClaudeCache: unchanged behaviour when no tool is deferred", () => {
const body = anchorClaudeCache({
messages: [{ role: "user", content: "hi" }],
tools: [tool("a"), tool("b")],
});
expect(body.tools[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
expect(body.tools[0].cache_control).toBeUndefined();
});
it("prepareClaudeRequest: deferred tail tool does not get the anchor", () => {
const out = prepareClaudeRequest({
model: "claude-sonnet-4.5",
messages: [{ role: "user", content: "hi" }],
tools: [tool("a"), tool("mcp__x__y", { defer_loading: true })],
}, "claude");
expect(out.tools).toHaveLength(2);
expect(out.tools[1].cache_control).toBeUndefined();
expect(out.tools[1].defer_loading).toBe(true);
expect(out.tools[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
});
});
+5
View File
@@ -55,12 +55,17 @@ describe("OpenCode Free endpoint routing", () => {
expect(opencode.transport.format).toBeUndefined();
const muse = opencode.models.find((m) => m.id === MUSE);
expect(muse?.targetFormat).toBe("openai-responses");
const muse13 = opencode.models.find((m) => m.id === "muse-spark-1.3-contributor-free");
expect(muse13?.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("muse-spark-1.3-contributor-free")).toBe("https://opencode.ai/zen/v1/responses");
expect(executor.buildUrl("muse-spark-1.4-contributor-free")).toBe("https://opencode.ai/zen/v1/responses");
expect(executor.buildUrl("muse-spark-2.0-contributor-free(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");
});
@@ -0,0 +1,74 @@
import { describe, it, expect, vi } from "vitest";
// sever the DB import chain (usageDb -> @/lib/db/*) — not under test
vi.mock("@/lib/usageDb.js", () => ({
saveRequestUsage: vi.fn(),
appendRequestLog: vi.fn(),
saveRequestDetail: vi.fn(),
}));
// and the stream/console-coloring utils that drag in the translator graph
vi.mock("../../open-sse/utils/stream.js", () => ({
COLORS: {},
formatSSE: vi.fn(),
}));
import { extractUsageFromResponse } from "../../open-sse/handlers/chatCore/requestDetail.js";
import { canonicalizeUsage } from "../../open-sse/utils/usageTracking.js";
// The three real-world usage shapes and how extractUsageFromResponse() must
// surface their cache-read count so canonicalizeUsage() produces a correct
// cached_tokens. Regression for non-streaming codex/Responses traffic, where
// cache reads were silently dropped and usage recorded cached_tokens: 0.
describe("extractUsageFromResponse cache surfaces", () => {
it("surfaces OpenAI Responses input_tokens_details.cached_tokens", () => {
// codex / /v1/responses shape: prompt is cache-INCLUSIVE
const out = extractUsageFromResponse({
usage: { input_tokens: 25421, output_tokens: 5, total_tokens: 25426,
input_tokens_details: { cached_tokens: 24320 } },
});
expect(out.cached_tokens).toBe(24320);
expect(out.prompt_tokens).toBe(25421);
expect(out.cache_read_input_tokens).toBeUndefined();
});
it("canonicalizes Responses usage without double-counting the prompt", () => {
const extracted = extractUsageFromResponse({
usage: { input_tokens: 25421, output_tokens: 5,
input_tokens_details: { cached_tokens: 24320 } },
});
const out = canonicalizeUsage(extracted);
// inclusive prompt passes through unchanged; cache reported as subset
expect(out.prompt_tokens).toBe(25421);
expect(out.cached_tokens).toBe(24320);
expect(out.total_tokens).toBe(25426);
expect(out.cache_creation_input_tokens).toBe(0);
});
it("still folds genuine Claude exclusive cache (regression)", () => {
const extracted = extractUsageFromResponse({
usage: { input_tokens: 100, output_tokens: 50,
cache_read_input_tokens: 200, cache_creation_input_tokens: 30 },
});
expect(extracted.cached_tokens).toBeUndefined();
const out = canonicalizeUsage(extracted);
expect(out.prompt_tokens).toBe(330); // 100 + 200 + 30
expect(out.cached_tokens).toBe(200);
expect(out.cache_creation_input_tokens).toBe(30);
});
it("surfaces flat cached_tokens on the OpenAI branch (SSE-to-JSON shape)", () => {
const out = extractUsageFromResponse({
usage: { prompt_tokens: 300, completion_tokens: 10, cached_tokens: 240 },
});
expect(out.cached_tokens).toBe(240);
});
it("keeps nested prompt_tokens_details.cached_tokens working (regression)", () => {
const out = extractUsageFromResponse({
usage: { prompt_tokens: 300, completion_tokens: 10,
prompt_tokens_details: { cached_tokens: 240 } },
});
expect(out.cached_tokens).toBe(240);
expect(canonicalizeUsage(out).cached_tokens).toBe(240);
});
});
@@ -44,7 +44,7 @@ vi.mock("@/sse/utils/logger.js", () => ({
}));
vi.mock("@/shared/utils/ssrfGuard.js", () => ({
assertPublicUrl: vi.fn(),
assertPublicUrlResolved: vi.fn(async () => {}),
}));
import { handleFetch } from "@/sse/handlers/fetch.js";
@@ -85,7 +85,40 @@ describe("web fetch account state", () => {
expect(mocks.clearAccountError).toHaveBeenCalledWith(
"jina-connection",
expect.objectContaining({ connectionName: "Jina Test" }),
"webfetch:jina-reader",
);
expect(mocks.getProviderCredentials).toHaveBeenCalledWith(
"jina-reader",
expect.any(Set),
"webfetch:jina-reader",
);
expect(mocks.markAccountUnavailable).not.toHaveBeenCalled();
});
it("scopes provider failures to web fetch", async () => {
mocks.handleFetchCore.mockResolvedValue({
success: false,
status: 429,
error: "quota exceeded",
});
mocks.markAccountUnavailable.mockResolvedValue({ shouldFallback: false });
const response = await handleFetch(new Request("http://localhost/v1/web/fetch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: "jina-reader",
url: "https://example.com/article",
}),
}));
expect(response.status).toBe(429);
expect(mocks.markAccountUnavailable).toHaveBeenCalledWith(
"jina-connection",
429,
"quota exceeded",
"jina-reader",
"webfetch:jina-reader",
);
});
});
+36
View File
@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
import antigravityRegistry from "../../open-sse/providers/registry/antigravity.js";
import geminiRegistry from "../../open-sse/providers/registry/gemini.js";
import { MODEL_PRICING } from "../../open-sse/providers/pricing.js";
describe("Gemini 3.8 Flash Support & Config", () => {
it("registers gemini-3.8-flash tiered models in antigravity provider registry", () => {
const agIds = antigravityRegistry.models.map(m => m.id);
expect(agIds).toContain("gemini-3.8-flash-high");
expect(agIds).toContain("gemini-3.8-flash-medium");
expect(agIds).toContain("gemini-3.8-flash-low");
expect(agIds).toContain("gemini-3.8-flash");
});
it("registers gemini-3.8-flash in gemini provider registry", () => {
const geminiIds = geminiRegistry.models.map(m => m.id);
expect(geminiIds).toContain("gemini-3.8-flash");
});
it("resolves capabilities correctly for gemini-3.8 models with official limits", () => {
const caps = getCapabilitiesForModel("antigravity", "gemini-3.8-flash-high");
expect(caps.vision).toBe(true);
expect(caps.reasoning).toBe(true);
expect(caps.thinkingFormat).toBe("gemini-level");
expect(caps.contextWindow).toBe(1048576);
expect(caps.maxOutput).toBe(65536);
});
it("defines pricing matching gemini-3.7-flash baseline", () => {
expect(MODEL_PRICING["gemini-3.8-flash"]).toEqual(MODEL_PRICING["gemini-3.7-flash"]);
expect(MODEL_PRICING["gemini-3.8-flash-high"]).toEqual(MODEL_PRICING["gemini-3.7-flash-high"]);
expect(MODEL_PRICING["gemini-3.8-flash-medium"]).toEqual(MODEL_PRICING["gemini-3.7-flash-medium"]);
expect(MODEL_PRICING["gemini-3.8-flash-low"]).toEqual(MODEL_PRICING["gemini-3.7-flash-low"]);
});
});
+100
View File
@@ -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.8 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.8-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.8-flash-${tier}(${tier})`);
expect(finalBody.model).toBe(`gemini-3.8-flash-${tier}`);
expect(finalBody.request.generationConfig.thinkingConfig).toEqual({
thinkingLevel: tier,
includeThoughts: true,
});
}
);
});
describe("Gemini 3.8 MITM model extraction", () => {
it.each(["high", "medium", "low"])("extracts the %s thinking tier for gemini-3.8-flash-tiered", (tier) => {
const body = Buffer.from(JSON.stringify({
request: { generationConfig: { thinkingConfig: { thinkingLevel: tier } } },
}));
expect(mitmConfig.extractModel(
"/v1internal/models/gemini-3.8-flash-tiered:streamGenerateContent",
body
)).toBe(`gemini-3.8-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.8-flash-tiered:streamGenerateContent",
body
)).toBe("gemini-3.8-flash-medium");
});
});
describe("Gemini 3.8 MITM tools and catalog", () => {
it("includes gemini-3.8-flash tiers in MITM_TOOLS defaultModels", () => {
const defaultModelIds = MITM_TOOLS.antigravity.defaultModels.map((m) => m.id);
expect(defaultModelIds).toContain("gemini-3.8-flash-high");
expect(defaultModelIds).toContain("gemini-3.8-flash-medium");
expect(defaultModelIds).toContain("gemini-3.8-flash-low");
});
it("exposes the direct Gemini 3.8 API models and pricing", () => {
const ids = gemini.models.map((model) => model.id);
expect(ids).toContain("gemini-3.8-flash");
expect(MODEL_PRICING["gemini-3.8-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.8-flash-high");
expect(agCatalog).toContain("gemini-3.8-flash-medium");
expect(agCatalog).toContain("gemini-3.8-flash-low");
});
});
+127
View File
@@ -0,0 +1,127 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
import {
USAGE_SUPPORTED_PROVIDERS,
USAGE_APIKEY_PROVIDERS,
} from "../../src/shared/constants/providers.js";
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
const MODELS_URL = "https://api.groq.com/openai/v1/models";
function response(body, { status = 200, headers = {} } = {}) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json", ...headers },
});
}
const RATE_LIMIT_HEADERS = {
"x-ratelimit-limit-requests": "14400",
"x-ratelimit-remaining-requests": "14370",
"x-ratelimit-reset-requests": "2m59.56s",
"x-ratelimit-limit-tokens": "18000",
"x-ratelimit-remaining-tokens": "17997",
"x-ratelimit-reset-tokens": "7.66s",
};
describe("groq registry usage flags", () => {
it("is listed for apikey quota dashboard", () => {
expect(USAGE_SUPPORTED_PROVIDERS).toContain("groq");
expect(USAGE_APIKEY_PROVIDERS).toContain("groq");
});
});
describe("getUsageForProvider(groq)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("GETs the models endpoint with Bearer apiKey", async () => {
proxyAwareFetch.mockResolvedValueOnce(
response({ data: [] }, { headers: RATE_LIMIT_HEADERS }),
);
const usage = await getUsageForProvider({
provider: "groq",
apiKey: "gsk_test",
});
expect(usage.message).toBeUndefined();
expect(usage.plan).toBe("Groq");
expect(proxyAwareFetch).toHaveBeenCalledTimes(1);
const [url, opts] = proxyAwareFetch.mock.calls[0];
expect(url).toBe(MODELS_URL);
expect(opts.method).toBe("GET");
expect(opts.headers.Authorization).toBe("Bearer gsk_test");
});
it("parses request + token rate-limit headers into quotas", async () => {
proxyAwareFetch.mockResolvedValueOnce(
response({ data: [] }, { headers: RATE_LIMIT_HEADERS }),
);
const usage = await getUsageForProvider({
provider: "groq",
apiKey: "gsk_test",
});
expect(usage.quotas["Requests"]).toMatchObject({
used: 30,
total: 14400,
unlimited: false,
});
expect(usage.quotas["Tokens"]).toMatchObject({
used: 3,
total: 18000,
unlimited: false,
});
// Duration-string reset headers resolve to a real future ISO timestamp.
expect(new Date(usage.quotas["Requests"].resetAt).getTime()).toBeGreaterThan(Date.now());
expect(new Date(usage.quotas["Tokens"].resetAt).getTime()).toBeGreaterThan(Date.now());
});
it("returns a soft message (not an error) when no rate-limit headers are present", async () => {
proxyAwareFetch.mockResolvedValueOnce(response({ data: [] }));
const usage = await getUsageForProvider({
provider: "groq",
apiKey: "gsk_test",
});
expect(usage.error).toBeUndefined();
expect(usage.message).toMatch(/no rate-limit data/i);
expect(usage.quotas).toEqual({});
});
it("returns message on missing key / 401", async () => {
const missing = await getUsageForProvider({ provider: "groq" });
expect(missing.message).toMatch(/api key/i);
expect(proxyAwareFetch).not.toHaveBeenCalled();
proxyAwareFetch.mockResolvedValueOnce(response({ error: "invalid_api_key" }, { status: 401 }));
const auth = await getUsageForProvider({ provider: "groq", apiKey: "bad" });
expect(auth.message).toMatch(/auth|key/i);
});
});
describe("parseQuotaData(groq)", () => {
it("forwards used/total/resetAt for the dashboard table", () => {
const rows = parseQuotaData("groq", {
plan: "Groq",
quotas: {
Requests: { used: 30, total: 14400, resetAt: "2026-01-01T00:03:00.000Z" },
Tokens: { used: 3, total: 18000, resetAt: "2026-01-01T00:00:08.000Z" },
},
});
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({ name: "Requests", used: 30, total: 14400 });
expect(rows[1]).toMatchObject({ name: "Tokens", used: 3, total: 18000 });
});
});
+192
View File
@@ -0,0 +1,192 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { intercept } = require("../../src/mitm/handlers/kiro.js");
const MODEL = "offline-test-model";
function makeResponseCollector() {
const chunks = [];
const response = {
headersSent: false,
statusCode: undefined,
ended: false,
writeHead(statusCode) {
this.statusCode = statusCode;
this.headersSent = true;
return this;
},
write(chunk) {
chunks.push(Buffer.from(chunk));
return true;
},
end(chunk) {
if (chunk !== undefined) chunks.push(Buffer.from(chunk));
this.ended = true;
this.headersSent = true;
return this;
},
};
return { response, chunks };
}
async function captureOpenAIRequest(request) {
const originalFetch = globalThis.fetch;
const { response, chunks } = makeResponseCollector();
let captured;
const fetchMock = vi.fn(async (url, init) => {
captured = { url: String(url), init };
return new Response("data: [DONE]\n\n", {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
});
globalThis.fetch = fetchMock;
try {
await intercept(
{ headers: { "x-test": "kiro-image-forwarding" } },
response,
Buffer.from(JSON.stringify(request)),
MODEL,
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(captured.url.endsWith("/v1/chat/completions")).toBe(true);
expect(captured.init.method).toBe("POST");
expect(response.statusCode).toBe(200);
expect(response.ended).toBe(true);
expect(chunks.length).toBeGreaterThan(0);
return JSON.parse(captured.init.body);
} finally {
if (originalFetch === undefined) delete globalThis.fetch;
else globalThis.fetch = originalFetch;
}
}
function image(format, bytes) {
return { format, source: { bytes } };
}
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("Kiro MITM inline image forwarding", () => {
it("forwards text and inline images as OpenAI image_url content parts", async () => {
const outboundBody = await captureOpenAIRequest({
conversationState: {
history: [],
currentMessage: {
userInputMessage: {
content: " Describe this ",
images: [image("png", "aGVsbG8=")],
},
},
},
});
expect(outboundBody).toMatchObject({
model: MODEL,
stream: true,
messages: [{
role: "user",
content: [
{ type: "text", text: "Describe this" },
{ type: "image_url", image_url: { url: "data:image/png;base64,aGVsbG8=" } },
],
}],
});
});
it("emits an image-only user turn even when tool results are present", async () => {
const outboundBody = await captureOpenAIRequest({
conversationState: {
history: [],
currentMessage: {
userInputMessage: {
content: " ",
images: [image("jpg", "LzlqLzQ=")],
userInputMessageContext: {
toolResults: [
{ toolUseId: "tool-a", content: [{ text: "first" }, { text: "result" }] },
{ toolUseId: "tool-b", content: [{ text: "second" }] },
],
},
},
},
},
});
expect(outboundBody.messages).toEqual([
{ role: "tool", tool_call_id: "tool-a", content: "first\nresult" },
{ role: "tool", tool_call_id: "tool-b", content: "second" },
{
role: "user",
content: [
{ type: "image_url", image_url: { url: "data:image/jpeg;base64,LzlqLzQ=" } },
],
},
]);
});
it("keeps historical images on their original user turn", async () => {
const outboundBody = await captureOpenAIRequest({
conversationState: {
history: [
{
userInputMessage: {
content: " historical evidence ",
images: [image("jpeg", "anBlZw=="), image("webp", "d2VicA==")],
},
},
{ assistantResponseMessage: { content: "assistant reply" } },
],
currentMessage: { userInputMessage: { content: "current question" } },
},
});
expect(outboundBody.messages).toEqual([
{
role: "user",
content: [
{ type: "text", text: "historical evidence" },
{ type: "image_url", image_url: { url: "data:image/jpeg;base64,anBlZw==" } },
{ type: "image_url", image_url: { url: "data:image/webp;base64,d2VicA==" } },
],
},
{ role: "assistant", content: "assistant reply" },
{ role: "user", content: "current question" },
]);
});
it("ignores malformed and unsupported image entries without changing text-only behavior", async () => {
const outboundBody = await captureOpenAIRequest({
conversationState: {
history: [],
currentMessage: {
userInputMessage: {
content: " keep this text ",
images: [
image("svg", "ignored"),
image("PNG", "ignored"),
image("jpeg", ""),
{ format: "gif", source: { bytes: 42 } },
null,
[],
],
},
},
},
});
expect(outboundBody.messages).toEqual([
{ role: "user", content: "keep this text" },
]);
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Regression: Claude Code appends `[1m]` to the model name when the
* 1M-context beta is on, so `/v1/messages` arrives with
* `model: "claude-opus-5[1m]"`. Nothing in 9router knows about the marker:
* it matches no combo, no alias and no `provider/model` pair, so the request
* is rejected at model resolution and the client reports
*
* There's an issue with the selected model (claude-opus-5[1m]).
* It may not exist or you may not have access to it.
*
* The capability itself rides in the `anthropic-beta` header, which is
* forwarded untouched, so stripping the marker is all that is needed.
*/
import { describe, it, expect } from "vitest";
import { stripModelContextMarker } from "../../open-sse/utils/modelMarkers.js";
describe("model context marker", () => {
it("strips the [1m] marker and reports it", () => {
expect(stripModelContextMarker("claude-opus-5[1m]")).toEqual({
model: "claude-opus-5",
contextMarker: "1m",
});
});
it("strips it from a provider-prefixed model too", () => {
expect(stripModelContextMarker("cc/claude-sonnet-4.5[1m]")).toEqual({
model: "cc/claude-sonnet-4.5",
contextMarker: "1m",
});
});
it("is case insensitive", () => {
expect(stripModelContextMarker("claude-opus-5[1M]").model).toBe("claude-opus-5");
});
it("leaves a plain model untouched", () => {
expect(stripModelContextMarker("claude-opus-5")).toEqual({
model: "claude-opus-5",
contextMarker: null,
});
});
it("only strips a trailing marker, never one inside the name", () => {
expect(stripModelContextMarker("weird[1m]name")).toEqual({
model: "weird[1m]name",
contextMarker: null,
});
});
it("tolerates a non-string model", () => {
expect(stripModelContextMarker(undefined)).toEqual({ model: undefined, contextMarker: null });
});
});
@@ -0,0 +1,112 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import REGISTRY from "../../open-sse/providers/registry/index.js";
import { handleFetchCore } from "../../open-sse/handlers/fetch/index.js";
import { AI_PROVIDERS, getProvidersByKind } from "@/shared/constants/providers.js";
const CONFIG = {
baseUrl: "https://ollama.com/api/web_fetch",
timeoutMs: 30000,
};
afterEach(() => {
vi.unstubAllGlobals();
});
describe("Ollama Cloud web fetch provider", () => {
it("registers web fetch on the existing Ollama Cloud connection", () => {
const entry = REGISTRY.find((candidate) => candidate.id === "ollama");
expect(entry).toMatchObject({
category: "freeTier",
serviceKinds: ["llm", "webFetch"],
fetchConfig: {
baseUrl: "https://ollama.com/api/web_fetch",
method: "POST",
authHeader: "bearer",
formats: ["markdown"],
},
});
expect(AI_PROVIDERS.ollama?.fetchConfig).toEqual(entry.fetchConfig);
expect(getProvidersByKind("webFetch").map((provider) => provider.id)).toContain("ollama");
});
it("calls Ollama with bearer auth and normalizes the response", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
title: "Example Domain",
content: "Hello from Ollama",
links: ["https://www.iana.org/domains/example"],
}), {
status: 200,
headers: { "Content-Type": "application/json" },
})));
const result = await handleFetchCore({
url: "https://example.com",
format: "markdown",
maxCharacters: 5,
provider: "ollama",
providerConfig: CONFIG,
credentials: { apiKey: "ollama-test-key" },
});
expect(result.success).toBe(true);
expect(global.fetch).toHaveBeenCalledTimes(1);
const [requestUrl, init] = global.fetch.mock.calls[0];
expect(requestUrl).toBe("https://ollama.com/api/web_fetch");
expect(init.method).toBe("POST");
expect(init.headers).toEqual({
"content-type": "application/json",
authorization: "Bearer ollama-test-key",
});
expect(JSON.parse(init.body)).toEqual({ url: "https://example.com" });
expect(result.data).toMatchObject({
provider: "ollama",
url: "https://example.com",
title: "Example Domain",
content: { format: "markdown", text: "Hello", length: 5 },
links: ["https://www.iana.org/domains/example"],
usage: { fetch_cost_usd: null },
});
});
it("returns the upstream status and error message", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(
JSON.stringify({ error: "invalid API key" }),
{ status: 401, headers: { "Content-Type": "application/json" } },
)));
const result = await handleFetchCore({
url: "https://example.com",
provider: "ollama",
providerConfig: CONFIG,
credentials: { apiKey: "bad-key" },
});
expect(result).toMatchObject({
success: false,
status: 401,
error: "invalid API key",
});
});
it("treats an empty successful response as an upstream error", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(null, {
status: 200,
headers: { "Content-Type": "application/json" },
})));
const result = await handleFetchCore({
url: "https://example.com",
provider: "ollama",
providerConfig: CONFIG,
credentials: { apiKey: "ollama-test-key" },
});
expect(result).toEqual({
success: false,
status: 502,
error: "Ollama returned an empty or invalid web fetch response",
});
});
});
+61 -20
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js";
import { PROVIDER_MODELS } from "../../open-sse/config/providerModels.js";
import { PROVIDER_MODELS, getModelTargetFormat } 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";
@@ -19,25 +19,31 @@ const input = [{
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",
]);
expect(PROVIDER_MODELS.oc?.some((model) => model.id === "muse-spark-1.3-contributor-free")).toBe(true);
for (const m of [MODEL, "muse-spark-1.3-contributor-free", "muse-spark-1.4-contributor-free", "muse-spark-2.0-contributor-free"]) {
expect(getCapabilitiesForModel(PROVIDER, m)).toMatchObject({
reasoning: true,
thinkingFormat: "openai",
contextWindow: 1048576,
maxOutput: 131072,
});
expect(getCapabilitiesForModel(PROVIDER, `oc/${m}`)).toMatchObject({
reasoning: true,
contextWindow: 1048576,
maxOutput: 131072,
});
expect(getThinkingLevels(PROVIDER, m)).toEqual([
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
]);
expect(getModelTargetFormat("oc", m)).toBe(FORMATS.OPENAI_RESPONSES);
expect(getModelTargetFormat("opencode", m)).toBe(FORMATS.OPENAI_RESPONSES);
expect(getModelTargetFormat("openrouter", m)).toBeNull();
}
});
it("clamps max to xhigh and emits the Responses reasoning shape", () => {
@@ -91,4 +97,39 @@ describe("OpenCode Free Muse Spark thinking", () => {
expect(out.max_output_tokens).toBe(131072);
expect(out.max_tokens).toBeUndefined();
});
it("routes muse-spark-1.3-contributor-free and future Muse Spark models to Responses API", () => {
const executor = new OpenCodeExecutor();
const futureModel = "muse-spark-1.4-contributor-free";
for (const m of ["muse-spark-1.3-contributor-free", futureModel]) {
expect(executor.buildUrl(m)).toBe("https://opencode.ai/zen/v1/responses");
expect(executor.buildUrl(`${m}(high)`)).toBe("https://opencode.ai/zen/v1/responses");
expect(getModelTargetFormat("oc", m)).toBe("openai-responses");
const body = {
model: `oc/${m}`,
messages: [{ role: "user", content: "Hello" }],
reasoning_effort: "high",
max_tokens: 2048,
};
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI_RESPONSES,
m,
body,
true,
{},
PROVIDER,
);
const out = executor.transformRequest(m, translated, true, {
connectionId: "opencode-muse-spark-13-test",
});
expect(out.reasoning).toEqual({ effort: "high", summary: "auto" });
expect(out.max_output_tokens).toBe(2048);
expect(out.max_tokens).toBeUndefined();
}
});
});
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import {
STATUS_FILTER_OPTIONS,
getConnectionStatus,
matchesStatusFilter,
} from "@/app/(dashboard)/dashboard/providers/utils.js";
describe("providers status filter", () => {
it("exposes all/active/inactive/none options", () => {
expect(STATUS_FILTER_OPTIONS.map((o) => o.value)).toEqual([
"all",
"active",
"inactive",
"none",
]);
});
it("classifies a provider with no connections as none", () => {
expect(getConnectionStatus({ total: 0, allDisabled: false })).toBe("none");
});
it("classifies a provider whose only connections are disabled as inactive", () => {
expect(getConnectionStatus({ total: 2, allDisabled: true })).toBe(
"inactive",
);
});
it("classifies a provider with at least one enabled connection as active", () => {
expect(getConnectionStatus({ total: 1, allDisabled: false })).toBe(
"active",
);
});
it("treats noAuth providers as active even with no stored connection", () => {
expect(getConnectionStatus({ total: 0, allDisabled: false }, true)).toBe(
"active",
);
});
it("matchesStatusFilter always passes for 'all'", () => {
expect(matchesStatusFilter("all", { total: 0, allDisabled: false })).toBe(
true,
);
});
it("matchesStatusFilter compares against the derived status", () => {
const disabledStats = { total: 3, allDisabled: true };
expect(matchesStatusFilter("inactive", disabledStats)).toBe(true);
expect(matchesStatusFilter("active", disabledStats)).toBe(false);
expect(matchesStatusFilter("none", disabledStats)).toBe(false);
});
});
+147
View File
@@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Regression coverage for #3714: the SSRF guard's literal-hostname/IP checks
// matched specific textual representations rather than the underlying address,
// so a different (but equivalent) representation slipped through. Each case
// below is a bypass the issue reported, or one found while fixing it.
const { lookupMock } = vi.hoisted(() => ({ lookupMock: vi.fn() }));
vi.mock("node:dns", () => ({
default: { promises: { lookup: lookupMock } },
promises: { lookup: lookupMock },
}));
const { assertPublicUrl, assertPublicUrlResolved, fetchPublic } = await import("../../src/shared/utils/ssrfGuard.js");
describe("assertPublicUrl: literal hostname/IP bypasses from #3714", () => {
it("blocks a trailing-dot FQDN the same as the bare hostname", () => {
expect(() => assertPublicUrl("http://localhost/")).toThrow();
expect(() => assertPublicUrl("http://localhost./")).toThrow();
expect(() => assertPublicUrl("http://LOCALHOST./")).toThrow();
});
it("blocks IPv4-mapped IPv6 loopback regardless of which textual form the URL parser picks", () => {
// WHATWG URL parsing normalizes dotted-decimal IPv4-in-IPv6 to hex form —
// the original regex only matched the dotted form.
expect(() => assertPublicUrl("http://[::ffff:127.0.0.1]/")).toThrow();
expect(() => assertPublicUrl("http://[::ffff:7f00:1]/")).toThrow(); // hex form directly
expect(() => assertPublicUrl("http://[0000::ffff:127.0.0.1]/")).toThrow();
});
it("blocks IPv4-mapped IPv6 cloud metadata address (169.254.169.254)", () => {
expect(() => assertPublicUrl("http://[::ffff:169.254.169.254]/")).toThrow();
expect(() => assertPublicUrl("http://[::ffff:a9fe:a9fe]/")).toThrow(); // hex form
});
it("blocks other loopback/private/link-local/ULA IPv6 forms", () => {
for (const url of [
"http://[::1]/",
"http://[::127.0.0.1]/",
"http://[fe80::1]/",
"http://[fc00::1]/",
"http://[fd12:3456::1]/",
"http://[64:ff9b::127.0.0.1]/", // NAT64 well-known prefix embedding a private IPv4
]) {
expect(() => assertPublicUrl(url), url).toThrow();
}
});
it("blocks alternate IPv4 literal encodings (already normalized by the URL parser)", () => {
for (const url of ["http://127.1/", "http://0177.0.0.1/", "http://2130706433/", "http://0x7f.0.0.1/"]) {
expect(() => assertPublicUrl(url), url).toThrow();
}
});
it("still allows public hosts, including public IPv6", () => {
expect(() => assertPublicUrl("https://api.openai.com/v1/models")).not.toThrow();
expect(() => assertPublicUrl("http://8.8.8.8/")).not.toThrow();
expect(() => assertPublicUrl("https://[2001:4860:4860::8888]/")).not.toThrow();
});
});
describe("assertPublicUrlResolved: DNS-resolving hostname bypass from #3714", () => {
beforeEach(() => lookupMock.mockReset());
it("blocks a hostname that resolves to a loopback address (nip.io-style wildcard DNS)", async () => {
lookupMock.mockResolvedValue([{ address: "127.0.0.1", family: 4 }]);
await expect(assertPublicUrlResolved("http://127.0.0.1.nip.io/")).rejects.toThrow();
});
it("blocks a hostname that resolves to a private range even if one of several addresses is public", async () => {
lookupMock.mockResolvedValue([{ address: "203.0.113.5", family: 4 }, { address: "10.0.0.5", family: 4 }]);
await expect(assertPublicUrlResolved("http://multi-a-record.example.test/")).rejects.toThrow();
});
it("blocks a hostname that resolves to a blocked IPv6 address", async () => {
lookupMock.mockResolvedValue([{ address: "::1", family: 6 }]);
await expect(assertPublicUrlResolved("http://evil.example.test/")).rejects.toThrow();
});
it("allows a hostname that resolves only to public addresses", async () => {
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
await expect(assertPublicUrlResolved("https://example.com/")).resolves.not.toThrow();
});
// Note: "fails open when the DNS lookup itself rejects" is deliberately not
// covered here as a vitest case — a mocked node:dns rejection in this file
// trips what looks like a vitest 4 / rolldown-transform source-map bug
// (the same rejection pattern passes in an isolated single-function probe
// module; only reproduces once mocked against this larger file). Verified
// instead with a standalone Node script exercising the real try/catch
// directly: dns.promises.lookup rejecting resolves assertPublicUrlResolved
// with undefined rather than propagating, exactly as the source shows.
it("skips DNS lookup entirely for literal IP hosts (already covered by the sync check)", async () => {
await expect(assertPublicUrlResolved("http://127.0.0.1/")).rejects.toThrow();
expect(lookupMock).not.toHaveBeenCalled();
});
});
describe("fetchPublic: redirect-target re-validation from #3714", () => {
const originalFetch = global.fetch;
afterEach(() => { global.fetch = originalFetch; });
beforeEach(() => {
lookupMock.mockReset();
// These tests exercise redirect-chasing, not DNS behavior — give every
// synthetic *.example.test hostname a default public resolution so it
// doesn't get blocked (or throw on an unmocked undefined return) before
// reaching the redirect logic under test.
lookupMock.mockResolvedValue([{ address: "203.0.113.10", family: 4 }]);
});
it("blocks a redirect from a validated public URL to an internal target", async () => {
global.fetch = vi.fn(async () => new Response(null, {
status: 302,
headers: { Location: "http://127.0.0.1:9999/admin" },
}));
await expect(fetchPublic("https://public.example.test/redirect")).rejects.toThrow();
expect(global.fetch).toHaveBeenCalledTimes(1); // never followed the redirect
});
it("follows a redirect chain of public URLs, re-validating each hop", async () => {
global.fetch = vi.fn()
.mockResolvedValueOnce(new Response(null, { status: 302, headers: { Location: "https://hop2.example.test/" } }))
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
const res = await fetchPublic("https://hop1.example.test/");
expect(await res.text()).toBe("ok");
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(global.fetch.mock.calls[1][0]).toBe("https://hop2.example.test/");
});
it("bounds the redirect chain instead of looping forever", async () => {
global.fetch = vi.fn(async (url) => new Response(null, {
status: 302,
headers: { Location: url === "https://loop.example.test/a" ? "https://loop.example.test/b" : "https://loop.example.test/a" },
}));
await expect(fetchPublic("https://loop.example.test/a", {}, { maxRedirects: 3 })).rejects.toThrow(/too many redirects/i);
});
it("rejects the initial URL before ever calling fetch", async () => {
global.fetch = vi.fn();
await expect(fetchPublic("http://127.0.0.1/steal")).rejects.toThrow();
expect(global.fetch).not.toHaveBeenCalled();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
buildModelsList: vi.fn(),
}));
vi.mock("../../src/app/api/v1/models/route.js", () => ({
buildModelsList: mocks.buildModelsList,
}));
const { GET } = await import("../../src/app/api/v1/models/[...model]/route.js");
const chatModel = {
id: "cc/claude-sonnet-5",
object: "model",
owned_by: "cc",
context_length: 1_000_000,
};
function params(model) {
return { params: Promise.resolve({ model }) };
}
describe("GET /v1/models/{id}", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("retrieves a provider-prefixed model ID split across URL path segments", async () => {
mocks.buildModelsList.mockResolvedValue([chatModel]);
const response = await GET(new Request("https://router.test/v1/models/cc/claude-sonnet-5"), params(["cc", "claude-sonnet-5"]));
expect(response.status).toBe(200);
expect(await response.json()).toEqual(chatModel);
expect(mocks.buildModelsList).toHaveBeenCalledWith(["llm"]);
});
it("also handles a decoded slash in a single catch-all segment", async () => {
mocks.buildModelsList.mockResolvedValue([chatModel]);
const response = await GET(new Request("https://router.test/v1/models/cc%2Fclaude-sonnet-5"), params(["cc/claude-sonnet-5"]));
expect(response.status).toBe(200);
expect(await response.json()).toEqual(chatModel);
});
it("keeps capability-list routes unchanged", async () => {
const imageModel = { id: "image/gpt-image-1", object: "model", owned_by: "image" };
mocks.buildModelsList.mockResolvedValue([imageModel]);
const response = await GET(new Request("https://router.test/v1/models/image"), params(["image"]));
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ object: "list", data: [imageModel] });
expect(mocks.buildModelsList).toHaveBeenCalledWith(["image"]);
});
it("returns an OpenAI-style model_not_found response for an unknown model", async () => {
mocks.buildModelsList.mockResolvedValue([chatModel]);
const response = await GET(new Request("https://router.test/v1/models/cc/missing-model"), params(["cc", "missing-model"]));
const body = await response.json();
expect(response.status).toBe(404);
expect(body.error).toMatchObject({
type: "invalid_request_error",
code: "model_not_found",
});
});
});