mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
fix(translator): ESM-safe registry + tool-id pairing + responses max_tokens; add real-creds tests
- translator/index.js: replace require() with static side-effect imports (ESM-safe), lazy-init registry maps to survive circular import order - openai-responses->openai: map max_output_tokens -> max_tokens (avoid leaking field upstream) - gemini/antigravity -> openai: derive deterministic tool_call id from name so functionCall/functionResponse pair correctly (fixes provider tool-pairing 400s) - add offline unit tests (finish-reason, usage, session-manager, ollama malformed args, const guard) - add real-creds integration tests (provider-cases + all-formats matrix: 6 inbound formats x 4 scenarios) Includes co-located provider registry refactor (pricing/capabilities/media providers) and sessionManager updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// A5 (cases #7/#9/#10): lock hardcode->config no-op values.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
OPENAI_COMPAT_BASE,
|
||||
ANTHROPIC_COMPAT_BASE,
|
||||
ANTHROPIC_API_VERSION,
|
||||
} from "../../open-sse/providers/shared.js";
|
||||
import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../open-sse/config/runtimeConfig.js";
|
||||
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";
|
||||
|
||||
describe("compat base URLs / version", () => {
|
||||
it("OPENAI_COMPAT_BASE", () => {
|
||||
expect(OPENAI_COMPAT_BASE).toBe("https://api.openai.com/v1");
|
||||
});
|
||||
it("ANTHROPIC_COMPAT_BASE", () => {
|
||||
expect(ANTHROPIC_COMPAT_BASE).toBe("https://api.anthropic.com/v1");
|
||||
});
|
||||
it("ANTHROPIC_API_VERSION", () => {
|
||||
expect(ANTHROPIC_API_VERSION).toBe("2023-06-01");
|
||||
});
|
||||
});
|
||||
|
||||
describe("default token limits", () => {
|
||||
it("max/min", () => {
|
||||
expect(DEFAULT_MAX_TOKENS).toBe(64000);
|
||||
expect(DEFAULT_MIN_TOKENS).toBe(32000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider baseUrl const (full path, no trailing slash)", () => {
|
||||
it("mimo-free full path", () => {
|
||||
expect(mimoFree.transport.baseUrl).toBe("https://api.xiaomimimo.com/api/free-ai/openai/chat");
|
||||
});
|
||||
it("opencode no trailing slash", () => {
|
||||
expect(opencode.transport.baseUrl).toBe("https://opencode.ai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("antigravity retry (intentional change: 429=6, 503=3)", () => {
|
||||
it("429 attempts = 6", () => {
|
||||
expect(antigravity.transport.retry["429"].attempts).toBe(6);
|
||||
});
|
||||
it("503 attempts = 3", () => {
|
||||
expect(antigravity.transport.retry["503"].attempts).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// A1: locks toOpenAIFinish/fromOpenAIFinish behavior changes vs open-sse.old.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { toOpenAIFinish, fromOpenAIFinish } from "../../open-sse/translator/concerns/finishReason.js";
|
||||
import { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "../../open-sse/translator/schema/finishReasons.js";
|
||||
|
||||
describe("toOpenAIFinish - gemini", () => {
|
||||
it.each([
|
||||
["SAFETY", "content_filter"],
|
||||
["RECITATION", "content_filter"],
|
||||
["BLOCKLIST", "content_filter"],
|
||||
["PROHIBITED_CONTENT", "content_filter"],
|
||||
["OTHER", "stop"],
|
||||
["UNKNOWN_XYZ", "stop"],
|
||||
["STOP", "stop"],
|
||||
["MAX_TOKENS", "length"],
|
||||
])("%s -> %s", (input, expected) => {
|
||||
expect(toOpenAIFinish(input, "gemini")).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toOpenAIFinish - ollama", () => {
|
||||
it.each([
|
||||
["length", "length"],
|
||||
["max_tokens", "length"],
|
||||
["tool_calls", "tool_calls"],
|
||||
["unknown_xyz", "stop"],
|
||||
])("%s -> %s", (input, expected) => {
|
||||
expect(toOpenAIFinish(input, "ollama")).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toOpenAIFinish - kiro", () => {
|
||||
it("tool_use -> tool_calls", () => {
|
||||
expect(toOpenAIFinish("tool_use", "kiro")).toBe("tool_calls");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toOpenAIFinish - claude", () => {
|
||||
it.each([
|
||||
["end_turn", "stop"],
|
||||
["max_tokens", "length"],
|
||||
["tool_use", "tool_calls"],
|
||||
])("%s -> %s", (input, expected) => {
|
||||
expect(toOpenAIFinish(input, "claude")).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toOpenAIFinish - commandcode", () => {
|
||||
it("tool-calls -> tool_calls", () => {
|
||||
expect(toOpenAIFinish("tool-calls", "commandcode")).toBe("tool_calls");
|
||||
});
|
||||
it("unknown passthrough", () => {
|
||||
expect(toOpenAIFinish("xyz", "commandcode")).toBe("xyz");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromOpenAIFinish round-trip - claude", () => {
|
||||
it("tool_calls -> tool_use", () => {
|
||||
expect(fromOpenAIFinish("tool_calls", "claude")).toBe("tool_use");
|
||||
});
|
||||
it("length -> max_tokens", () => {
|
||||
expect(fromOpenAIFinish("length", "claude")).toBe("max_tokens");
|
||||
});
|
||||
});
|
||||
|
||||
describe("enum literals (catch drift)", () => {
|
||||
it("OPENAI_FINISH literals", () => {
|
||||
expect(OPENAI_FINISH.STOP).toBe("stop");
|
||||
expect(OPENAI_FINISH.LENGTH).toBe("length");
|
||||
expect(OPENAI_FINISH.TOOL_CALLS).toBe("tool_calls");
|
||||
expect(OPENAI_FINISH.CONTENT_FILTER).toBe("content_filter");
|
||||
});
|
||||
it("CLAUDE_STOP literals", () => {
|
||||
expect(CLAUDE_STOP.END_TURN).toBe("end_turn");
|
||||
expect(CLAUDE_STOP.MAX_TOKENS).toBe("max_tokens");
|
||||
expect(CLAUDE_STOP.TOOL_USE).toBe("tool_use");
|
||||
});
|
||||
it("GEMINI_FINISH literals", () => {
|
||||
expect(GEMINI_FINISH.STOP).toBe("STOP");
|
||||
expect(GEMINI_FINISH.MAX_TOKENS).toBe("MAX_TOKENS");
|
||||
expect(GEMINI_FINISH.SAFETY).toBe("SAFETY");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// A4 (case #10): malformed tool_calls args must not throw -> safeParseJSON returns {}.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { openaiToOllamaRequest } from "../../open-sse/translator/request/openai-to-ollama.js";
|
||||
|
||||
function reqWith(args) {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "",
|
||||
tool_calls: [{ id: "c1", type: "function", function: { name: "get_weather", arguments: args } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("openaiToOllamaRequest - tool_calls arguments parsing", () => {
|
||||
it("malformed JSON args -> {} (no throw)", () => {
|
||||
let out;
|
||||
expect(() => {
|
||||
out = openaiToOllamaRequest("m", reqWith("{invalid json"), true);
|
||||
}).not.toThrow();
|
||||
expect(out.messages[0].tool_calls[0].function.arguments).toEqual({});
|
||||
});
|
||||
|
||||
it("valid JSON args -> parsed object", () => {
|
||||
const out = openaiToOllamaRequest("m", reqWith('{"a":1}'), true);
|
||||
expect(out.messages[0].tool_calls[0].function.arguments).toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { MODEL_PRICING } from "../../src/shared/constants/pricing.js";
|
||||
import { MODEL_PRICING } from "../../open-sse/providers/pricing.js";
|
||||
|
||||
describe("MiniMax-M3 pricing", () => {
|
||||
it("includes MiniMax-M3 in MODEL_PRICING", () => {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// A2: locks resolveSessionId priority/stickiness (codex/kiro/antigravity centralization).
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { resolveSessionId, deriveSessionId, clearSessionStore } from "../../open-sse/utils/sessionManager.js";
|
||||
|
||||
// Assistant text must exceed ASSISTANT_MIN_LEN (50) to trigger sticky hash path.
|
||||
const longAssistant = "x".repeat(80);
|
||||
const bodyWithAssistant = { messages: [{ role: "assistant", content: longAssistant }] };
|
||||
|
||||
beforeEach(() => clearSessionStore());
|
||||
|
||||
describe("resolveSessionId", () => {
|
||||
it("stickiness: same body+connectionId+scope -> same id", () => {
|
||||
const opts = { body: bodyWithAssistant, connectionId: "conn1", scope: "codex" };
|
||||
expect(resolveSessionId(opts)).toBe(resolveSessionId(opts));
|
||||
});
|
||||
|
||||
it("different connectionId -> different id", () => {
|
||||
const a = resolveSessionId({ body: bodyWithAssistant, connectionId: "connA", scope: "codex" });
|
||||
const b = resolveSessionId({ body: bodyWithAssistant, connectionId: "connB", scope: "codex" });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("different scope -> different id", () => {
|
||||
const a = resolveSessionId({ body: bodyWithAssistant, connectionId: "conn1", scope: "codex" });
|
||||
const b = resolveSessionId({ body: bodyWithAssistant, connectionId: "conn1", scope: "kiro" });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("fallback: empty body+no header+no workspaceId -> deriveSessionId(connectionId)", () => {
|
||||
const got = resolveSessionId({ body: {}, connectionId: "connFallback" });
|
||||
expect(got).toBe(deriveSessionId("connFallback"));
|
||||
});
|
||||
|
||||
it("client override: x-session-id header wins, skips later steps", () => {
|
||||
const got = resolveSessionId({
|
||||
headers: { "x-session-id": "client-sess-123" },
|
||||
body: bodyWithAssistant,
|
||||
connectionId: "conn1",
|
||||
workspaceId: "ws1",
|
||||
scope: "codex",
|
||||
});
|
||||
expect(got).toBe("client-sess-123");
|
||||
});
|
||||
|
||||
it("workspaceId path: empty body + workspaceId set -> normalized workspaceId", () => {
|
||||
const got = resolveSessionId({ body: {}, connectionId: "conn1", workspaceId: "ws-abc" });
|
||||
expect(got).toBe("ws-abc");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
// A3: locks toOpenAIUsage per-provider token math (claude/gemini/kiro/ollama/commandcode).
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { toOpenAIUsage } from "../../open-sse/translator/concerns/usage.js";
|
||||
|
||||
describe("toOpenAIUsage", () => {
|
||||
it("claude: folds cache read+create into prompt, exposes details", () => {
|
||||
const u = toOpenAIUsage(
|
||||
{ input_tokens: 100, output_tokens: 20, cache_read_input_tokens: 30, cache_creation_input_tokens: 10 },
|
||||
"claude"
|
||||
);
|
||||
expect(u.prompt_tokens).toBe(140);
|
||||
expect(u.completion_tokens).toBe(20);
|
||||
expect(u.total_tokens).toBe(160);
|
||||
expect(u.prompt_tokens_details.cached_tokens).toBe(30);
|
||||
expect(u.prompt_tokens_details.cache_creation_tokens).toBe(10);
|
||||
});
|
||||
|
||||
it("claude: no cache -> no prompt_tokens_details", () => {
|
||||
const u = toOpenAIUsage({ input_tokens: 50, output_tokens: 5 }, "claude");
|
||||
expect(u.prompt_tokens).toBe(50);
|
||||
expect(u.prompt_tokens_details).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gemini: full fields, completion = candidates + thoughts", () => {
|
||||
const u = toOpenAIUsage(
|
||||
{ promptTokenCount: 100, candidatesTokenCount: 40, thoughtsTokenCount: 10, totalTokenCount: 150 },
|
||||
"gemini"
|
||||
);
|
||||
expect(u.prompt_tokens).toBe(100);
|
||||
expect(u.completion_tokens).toBe(50);
|
||||
expect(u.total_tokens).toBe(150);
|
||||
expect(u.completion_tokens_details.reasoning_tokens).toBe(10);
|
||||
});
|
||||
|
||||
it("gemini fallback: candidates=0 -> derive from total - prompt - thoughts", () => {
|
||||
const u = toOpenAIUsage(
|
||||
{ promptTokenCount: 100, candidatesTokenCount: 0, thoughtsTokenCount: 10, totalTokenCount: 150 },
|
||||
"gemini"
|
||||
);
|
||||
// candidates derived = 150 - 100 - 10 = 40 ; completion = 40 + 10
|
||||
expect(u.completion_tokens).toBe(50);
|
||||
});
|
||||
|
||||
it("kiro: input/output straight", () => {
|
||||
const u = toOpenAIUsage({ inputTokens: 12, outputTokens: 3 }, "kiro");
|
||||
expect(u.prompt_tokens).toBe(12);
|
||||
expect(u.completion_tokens).toBe(3);
|
||||
expect(u.total_tokens).toBe(15);
|
||||
});
|
||||
|
||||
it("ollama: prompt_eval_count/eval_count", () => {
|
||||
const u = toOpenAIUsage({ prompt_eval_count: 7, eval_count: 4 }, "ollama");
|
||||
expect(u.prompt_tokens).toBe(7);
|
||||
expect(u.completion_tokens).toBe(4);
|
||||
expect(u.total_tokens).toBe(11);
|
||||
});
|
||||
|
||||
it("commandcode: keeps totalTokens fallback", () => {
|
||||
const u = toOpenAIUsage({ inputTokens: 8, outputTokens: 2, totalTokens: 99 }, "commandcode");
|
||||
expect(u.prompt_tokens).toBe(8);
|
||||
expect(u.completion_tokens).toBe(2);
|
||||
expect(u.total_tokens).toBe(99);
|
||||
});
|
||||
|
||||
it("unknown kind / null raw -> null", () => {
|
||||
expect(toOpenAIUsage({}, "nope")).toBeNull();
|
||||
expect(toOpenAIUsage(null, "claude")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user