mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (ff57be32f) and the merge-to-main (ae6e2342d), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (#5148) - fix(sse): preserve non-stream reasoning fields (#5155, @rdself) - fix(i18n): add missing English UI labels (#5153, @rdself) - test(combo): gated live smoke (#5151) + release-expectations refresh (#5150, @KooshaPari) (#5129 exact-host Anthropic baseUrl is already covered by the #5130 bullet — same CodeQL #674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule #20 (#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule #20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (#5134) (#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (#5154) (#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (#5166) (#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (#5123) (#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (#5123); 4/4 tests pass (incl. #4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (#5083) (#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace #5083 global middleware CSP with declarative ws: scheme (#5083) Follow-up to PR #5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from #5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (#4546) (#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (#5169) (#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (#5192) (#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes #5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (#5172) (#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (#2939 unchanged). Also addresses #5160 (same OOM root); #5152 (docker) benefits via the same knob. Closes #5172 * fix(proxy): coalesce fast-fail health probes (#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (#3850) (#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes #3850 * fix(responses): normalize non-array input (#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
516 lines
20 KiB
TypeScript
516 lines
20 KiB
TypeScript
/**
|
||
* Unit tests — xAI thinking patcher + inbound translators
|
||
*
|
||
* Ported from upstream decolua/9router tests/unit/xai-thinking.test.js
|
||
* and expanded with coverage for claude/gemini/openai-chat/openai-responses translators.
|
||
*
|
||
* Runner: node --import tsx/esm --test tests/unit/xai-translators.test.ts
|
||
*/
|
||
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
|
||
const { budgetToEffort, applyThinking, normalizeXaiReasoningEffort } =
|
||
await import("../../src/lib/providers/xai/thinking.ts");
|
||
const { chatRequestToXaiResponses, xaiCompletedToChatJson } =
|
||
await import("../../src/lib/providers/xai/translators/openai-chat.ts");
|
||
const { openaiResponsesRequestToXai, xaiCompletedToOpenaiResponses, xaiSseEventToOpenaiResponses } =
|
||
await import("../../src/lib/providers/xai/translators/openai-responses.ts");
|
||
const { claudeRequestToXaiResponses, xaiCompletedToClaudeJson } =
|
||
await import("../../src/lib/providers/xai/translators/claude.ts");
|
||
const { geminiRequestToXaiResponses, xaiCompletedToGeminiJson } =
|
||
await import("../../src/lib/providers/xai/translators/gemini.ts");
|
||
|
||
// ─── budgetToEffort ──────────────────────────────────────────────────────────
|
||
|
||
test("budgetToEffort: maps 0 / negative / NaN to undefined", () => {
|
||
assert.equal(budgetToEffort(0), undefined);
|
||
assert.equal(budgetToEffort(-100), undefined);
|
||
assert.equal(budgetToEffort(Number.NaN), undefined);
|
||
assert.equal(budgetToEffort(Number.POSITIVE_INFINITY), undefined);
|
||
});
|
||
|
||
test("budgetToEffort: maps 1–3999 to low", () => {
|
||
assert.equal(budgetToEffort(1), "low");
|
||
assert.equal(budgetToEffort(3999), "low");
|
||
});
|
||
|
||
test("budgetToEffort: maps 4000–15999 to medium", () => {
|
||
assert.equal(budgetToEffort(4000), "medium");
|
||
assert.equal(budgetToEffort(15999), "medium");
|
||
});
|
||
|
||
test("budgetToEffort: maps 16000+ to high", () => {
|
||
assert.equal(budgetToEffort(16000), "high");
|
||
assert.equal(budgetToEffort(64000), "high");
|
||
});
|
||
|
||
// ─── applyThinking ───────────────────────────────────────────────────────────
|
||
|
||
test("applyThinking: returns clone untouched when nothing matches", () => {
|
||
const req = { model: "grok-4", input: [] };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out, req);
|
||
assert.notStrictEqual(out, req); // must be a new object
|
||
});
|
||
|
||
test("applyThinking: honors xAI-native reasoning.effort verbatim", () => {
|
||
const req = { reasoning: { effort: "high" }, foo: 1 };
|
||
const out = applyThinking(req as Parameters<typeof applyThinking>[0]);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
assert.equal((out as Record<string, unknown>).foo, 1);
|
||
});
|
||
|
||
test("normalizeXaiReasoningEffort: downgrades max/xhigh to xAI-supported high", () => {
|
||
assert.equal(normalizeXaiReasoningEffort("max"), "high");
|
||
assert.equal(normalizeXaiReasoningEffort("xhigh"), "high");
|
||
assert.equal(normalizeXaiReasoningEffort("HIGH"), "high");
|
||
assert.equal(normalizeXaiReasoningEffort("ultra"), undefined);
|
||
});
|
||
|
||
test("applyThinking: normalizes xAI-native max/xhigh to high", () => {
|
||
const maxOut = applyThinking({ reasoning: { effort: "max", summary: "auto" } });
|
||
assert.deepStrictEqual(maxOut.reasoning, { effort: "high", summary: "auto" });
|
||
|
||
const xhighOut = applyThinking({ reasoning: { effort: "xhigh" } });
|
||
assert.deepStrictEqual(xhighOut.reasoning, { effort: "high" });
|
||
});
|
||
|
||
test("applyThinking: rewrites OpenAI Chat reasoning_effort into reasoning.effort", () => {
|
||
const req = { reasoning_effort: "medium" };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "medium" });
|
||
assert.equal(out.reasoning_effort, undefined);
|
||
});
|
||
|
||
test("applyThinking: rewrites OpenAI Chat max reasoning_effort into high", () => {
|
||
const req = { reasoning_effort: "max" };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
assert.equal(out.reasoning_effort, undefined);
|
||
});
|
||
|
||
test("applyThinking: ignores invalid reasoning_effort values", () => {
|
||
const req = { reasoning_effort: "ultra" };
|
||
const out = applyThinking(req);
|
||
assert.equal(out.reasoning, undefined);
|
||
});
|
||
|
||
test("applyThinking: maps Anthropic thinking enabled with budget_tokens", () => {
|
||
const req = { thinking: { type: "enabled", budget_tokens: 20000 } };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
assert.equal(out.thinking, undefined);
|
||
});
|
||
|
||
test("applyThinking: defaults Anthropic thinking enabled without budget_tokens to medium", () => {
|
||
const req = { thinking: { type: "enabled" } };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "medium" });
|
||
});
|
||
|
||
test("applyThinking: strips Anthropic thinking type=disabled without setting reasoning", () => {
|
||
const req = { thinking: { type: "disabled" } };
|
||
const out = applyThinking(req);
|
||
assert.equal(out.reasoning, undefined);
|
||
assert.equal(out.thinking, undefined);
|
||
});
|
||
|
||
test("applyThinking: maps Gemini thinkingConfig.thinkingBudget", () => {
|
||
const req = { thinkingConfig: { thinkingBudget: 5000 } };
|
||
const out = applyThinking(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "medium" });
|
||
assert.equal(out.thinkingConfig, undefined);
|
||
});
|
||
|
||
test("applyThinking: strips Gemini thinkingConfig with budget=0 without setting reasoning", () => {
|
||
const req = { thinkingConfig: { thinkingBudget: 0 } };
|
||
const out = applyThinking(req);
|
||
assert.equal(out.reasoning, undefined);
|
||
assert.equal(out.thinkingConfig, undefined);
|
||
});
|
||
|
||
test("applyThinking: applies defaultEffort when nothing else is provided", () => {
|
||
const out = applyThinking({}, { defaultEffort: "low" });
|
||
assert.deepStrictEqual(out.reasoning, { effort: "low" });
|
||
});
|
||
|
||
// ─── chatRequestToXaiResponses ────────────────────────────────────────────────
|
||
|
||
test("chatRequestToXaiResponses: converts system message to instructions", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [
|
||
{ role: "system", content: "You are a helpful assistant." },
|
||
{ role: "user", content: "Hello" },
|
||
],
|
||
};
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.equal(out.instructions, "You are a helpful assistant.");
|
||
assert.equal(out.input.length, 1);
|
||
assert.equal((out.input[0] as Record<string, unknown>).role, "user");
|
||
});
|
||
|
||
test("chatRequestToXaiResponses: converts tool message to function_call_output", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [{ role: "tool", content: "result text", tool_call_id: "call_abc" }],
|
||
};
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.equal(out.input[0].type, "function_call_output");
|
||
assert.equal(out.input[0].call_id, "call_abc");
|
||
assert.equal(out.input[0].output, "result text");
|
||
});
|
||
|
||
test("chatRequestToXaiResponses: promotes reasoning_effort to reasoning field", () => {
|
||
const req = { model: "grok-4", messages: [], reasoning_effort: "high" };
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
});
|
||
|
||
test("chatRequestToXaiResponses: normalizes max reasoning_effort for xAI", () => {
|
||
const req = { model: "grok-4.3", messages: [], reasoning_effort: "max" };
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
});
|
||
|
||
test("chatRequestToXaiResponses: normalizes reasoning.effort for xAI", () => {
|
||
const req = { model: "grok-4.3", messages: [], reasoning: { effort: "max", summary: "auto" } };
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high", summary: "auto" });
|
||
});
|
||
|
||
test("chatRequestToXaiResponses: maps max_tokens to max_output_tokens", () => {
|
||
const req = { model: "grok-4", messages: [], max_tokens: 512 };
|
||
const out = chatRequestToXaiResponses(req);
|
||
assert.equal(out.max_output_tokens, 512);
|
||
});
|
||
|
||
// ─── xaiCompletedToChatJson ──────────────────────────────────────────────────
|
||
|
||
test("xaiCompletedToChatJson: extracts output_text content into message", () => {
|
||
const completed = {
|
||
id: "resp_1",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "message",
|
||
content: [{ type: "output_text", text: "Hello!" }],
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToChatJson(completed) as Record<string, unknown>;
|
||
const choices = result.choices as Array<Record<string, unknown>>;
|
||
assert.equal(choices[0].finish_reason, "stop");
|
||
const message = choices[0].message as Record<string, unknown>;
|
||
assert.equal(message.content, "Hello!");
|
||
});
|
||
|
||
test("xaiCompletedToChatJson: maps function_call to tool_calls with finish_reason=tool_calls", () => {
|
||
const completed = {
|
||
id: "resp_2",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "function_call",
|
||
call_id: "call_1",
|
||
name: "get_weather",
|
||
arguments: '{"location":"London"}',
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToChatJson(completed) as Record<string, unknown>;
|
||
const choices = result.choices as Array<Record<string, unknown>>;
|
||
assert.equal(choices[0].finish_reason, "tool_calls");
|
||
const message = choices[0].message as Record<string, unknown>;
|
||
const toolCalls = message.tool_calls as Array<Record<string, unknown>>;
|
||
assert.equal(toolCalls.length, 1);
|
||
assert.equal(toolCalls[0].id, "call_1");
|
||
const fn = toolCalls[0].function as Record<string, unknown>;
|
||
assert.equal(fn.name, "get_weather");
|
||
});
|
||
|
||
// ─── openaiResponsesRequestToXai ─────────────────────────────────────────────
|
||
|
||
test("openaiResponsesRequestToXai: drops service_tier", () => {
|
||
const req = { model: "grok-4", input: [], service_tier: "default" };
|
||
const out = openaiResponsesRequestToXai(req);
|
||
assert.equal("service_tier" in out, false);
|
||
});
|
||
|
||
test("openaiResponsesRequestToXai: preserves other fields verbatim", () => {
|
||
const req = { model: "grok-4", input: [{ role: "user", content: "hi" }] };
|
||
const out = openaiResponsesRequestToXai(req);
|
||
assert.deepStrictEqual(out.input, req.input);
|
||
});
|
||
|
||
// ─── xaiCompletedToOpenaiResponses ───────────────────────────────────────────
|
||
|
||
test("xaiCompletedToOpenaiResponses: normalizes object + status fields", () => {
|
||
const completed = { id: "r1", model: "grok-4", output: [] };
|
||
const out = xaiCompletedToOpenaiResponses(completed);
|
||
assert.equal(out.object, "response");
|
||
assert.equal(out.status, "completed");
|
||
});
|
||
|
||
test("xaiCompletedToOpenaiResponses: preserves existing object/status", () => {
|
||
const completed = { id: "r1", object: "custom", status: "in_progress" };
|
||
const out = xaiCompletedToOpenaiResponses(completed);
|
||
assert.equal(out.object, "custom");
|
||
assert.equal(out.status, "in_progress");
|
||
});
|
||
|
||
// ─── xaiSseEventToOpenaiResponses ────────────────────────────────────────────
|
||
|
||
test("xaiSseEventToOpenaiResponses: drops annotation.added event", () => {
|
||
const ev = {
|
||
event: "response.output_text.annotation.added",
|
||
data: '{"text":"foo"}',
|
||
};
|
||
const result = xaiSseEventToOpenaiResponses(ev);
|
||
assert.equal(result, null);
|
||
});
|
||
|
||
test("xaiSseEventToOpenaiResponses: passes through other events unchanged", () => {
|
||
const ev = { event: "response.output_text.delta", data: '{"delta":"hi"}' };
|
||
const result = xaiSseEventToOpenaiResponses(ev);
|
||
assert.deepStrictEqual(result, ev);
|
||
});
|
||
|
||
// ─── claudeRequestToXaiResponses ─────────────────────────────────────────────
|
||
|
||
test("claudeRequestToXaiResponses: translates system string to instructions", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
system: "Be helpful",
|
||
messages: [{ role: "user" as const, content: "Hi" }],
|
||
};
|
||
const out = claudeRequestToXaiResponses(req);
|
||
assert.equal(out.instructions, "Be helpful");
|
||
});
|
||
|
||
test("claudeRequestToXaiResponses: converts text content to input_text block", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [{ role: "user" as const, content: "Hello" }],
|
||
};
|
||
const out = claudeRequestToXaiResponses(req);
|
||
assert.equal(out.input.length, 1);
|
||
const inputItem = out.input[0] as Record<string, unknown>;
|
||
const content = inputItem.content as Array<Record<string, unknown>>;
|
||
assert.equal(content[0].type, "input_text");
|
||
assert.equal(content[0].text, "Hello");
|
||
});
|
||
|
||
test("claudeRequestToXaiResponses: extracts tool_result to function_call_output", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [
|
||
{
|
||
role: "user" as const,
|
||
content: [
|
||
{
|
||
type: "tool_result",
|
||
tool_use_id: "tu_123",
|
||
content: "result data",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const out = claudeRequestToXaiResponses(req);
|
||
const item = out.input[0] as Record<string, unknown>;
|
||
assert.equal(item.type, "function_call_output");
|
||
assert.equal(item.call_id, "tu_123");
|
||
assert.equal(item.output, "result data");
|
||
});
|
||
|
||
test("claudeRequestToXaiResponses: translates tools from Anthropic to xAI function shape", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [],
|
||
tools: [
|
||
{
|
||
name: "search",
|
||
description: "Search the web",
|
||
input_schema: { type: "object", properties: {} },
|
||
},
|
||
],
|
||
};
|
||
const out = claudeRequestToXaiResponses(req);
|
||
assert.ok(Array.isArray(out.tools));
|
||
const tool = (out.tools as Array<Record<string, unknown>>)[0];
|
||
assert.equal(tool.type, "function");
|
||
const fn = tool.function as Record<string, unknown>;
|
||
assert.equal(fn.name, "search");
|
||
});
|
||
|
||
test("claudeRequestToXaiResponses: maps thinking.enabled with budget to reasoning", () => {
|
||
const req = {
|
||
model: "grok-4",
|
||
messages: [],
|
||
thinking: { type: "enabled", budget_tokens: 8000 },
|
||
};
|
||
const out = claudeRequestToXaiResponses(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "medium" });
|
||
});
|
||
|
||
// ─── xaiCompletedToClaudeJson ─────────────────────────────────────────────────
|
||
|
||
test("xaiCompletedToClaudeJson: converts output_text to text content block", () => {
|
||
const completed = {
|
||
id: "r1",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "message",
|
||
content: [{ type: "output_text", text: "Hello!" }],
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToClaudeJson(completed) as Record<string, unknown>;
|
||
assert.equal(result.role, "assistant");
|
||
assert.equal(result.stop_reason, "end_turn");
|
||
const content = result.content as Array<Record<string, unknown>>;
|
||
assert.equal(content[0].type, "text");
|
||
assert.equal(content[0].text, "Hello!");
|
||
});
|
||
|
||
test("xaiCompletedToClaudeJson: converts function_call to tool_use block", () => {
|
||
const completed = {
|
||
id: "r2",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "function_call",
|
||
call_id: "c1",
|
||
name: "lookup",
|
||
arguments: '{"q":"test"}',
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToClaudeJson(completed) as Record<string, unknown>;
|
||
assert.equal(result.stop_reason, "tool_use");
|
||
const content = result.content as Array<Record<string, unknown>>;
|
||
assert.equal(content[0].type, "tool_use");
|
||
assert.equal(content[0].name, "lookup");
|
||
});
|
||
|
||
// ─── geminiRequestToXaiResponses ─────────────────────────────────────────────
|
||
|
||
test("geminiRequestToXaiResponses: converts text parts to input_text blocks", () => {
|
||
const req = {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
};
|
||
const out = geminiRequestToXaiResponses(req, "grok-4");
|
||
assert.equal(out.model, "grok-4");
|
||
const item = out.input[0] as Record<string, unknown>;
|
||
const content = item.content as Array<Record<string, unknown>>;
|
||
assert.equal(content[0].type, "input_text");
|
||
assert.equal(content[0].text, "Hello");
|
||
});
|
||
|
||
test("geminiRequestToXaiResponses: maps systemInstruction to instructions", () => {
|
||
const req = {
|
||
contents: [],
|
||
systemInstruction: { parts: [{ text: "Be helpful" }] },
|
||
};
|
||
const out = geminiRequestToXaiResponses(req);
|
||
assert.equal(out.instructions, "Be helpful");
|
||
});
|
||
|
||
test("geminiRequestToXaiResponses: converts functionDeclarations to xAI tools", () => {
|
||
const req = {
|
||
contents: [],
|
||
tools: [
|
||
{
|
||
functionDeclarations: [
|
||
{
|
||
name: "search",
|
||
description: "Search the web",
|
||
parameters: { type: "object" },
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const out = geminiRequestToXaiResponses(req);
|
||
assert.ok(Array.isArray(out.tools));
|
||
const tool = (out.tools as Array<Record<string, unknown>>)[0];
|
||
assert.equal(tool.type, "function");
|
||
const fn = tool.function as Record<string, unknown>;
|
||
assert.equal(fn.name, "search");
|
||
});
|
||
|
||
test("geminiRequestToXaiResponses: maps thinkingBudget to reasoning.effort", () => {
|
||
const req = {
|
||
contents: [],
|
||
generationConfig: { thinkingConfig: { thinkingBudget: 20000 } },
|
||
};
|
||
const out = geminiRequestToXaiResponses(req);
|
||
assert.deepStrictEqual(out.reasoning, { effort: "high" });
|
||
});
|
||
|
||
test("geminiRequestToXaiResponses: converts model role to assistant", () => {
|
||
const req = {
|
||
contents: [{ role: "model", parts: [{ text: "Hi" }] }],
|
||
};
|
||
const out = geminiRequestToXaiResponses(req);
|
||
const item = out.input[0] as Record<string, unknown>;
|
||
assert.equal(item.role, "assistant");
|
||
});
|
||
|
||
// ─── xaiCompletedToGeminiJson ────────────────────────────────────────────────
|
||
|
||
test("xaiCompletedToGeminiJson: converts output_text to Gemini candidate text part", () => {
|
||
const completed = {
|
||
id: "r1",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "message",
|
||
content: [{ type: "output_text", text: "Hello Gemini!" }],
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToGeminiJson(completed) as Record<string, unknown>;
|
||
const candidates = result.candidates as Array<Record<string, unknown>>;
|
||
const content = candidates[0].content as Record<string, unknown>;
|
||
const parts = content.parts as Array<Record<string, unknown>>;
|
||
assert.equal(parts[0].text, "Hello Gemini!");
|
||
assert.equal(candidates[0].finishReason, "STOP");
|
||
});
|
||
|
||
test("xaiCompletedToGeminiJson: converts function_call to functionCall part", () => {
|
||
const completed = {
|
||
id: "r2",
|
||
model: "grok-4",
|
||
output: [
|
||
{
|
||
type: "function_call",
|
||
name: "lookup",
|
||
arguments: '{"q":"test"}',
|
||
},
|
||
],
|
||
};
|
||
const result = xaiCompletedToGeminiJson(completed) as Record<string, unknown>;
|
||
const candidates = result.candidates as Array<Record<string, unknown>>;
|
||
const content = candidates[0].content as Record<string, unknown>;
|
||
const parts = content.parts as Array<Record<string, unknown>>;
|
||
const fc = parts[0].functionCall as Record<string, unknown>;
|
||
assert.equal(fc.name, "lookup");
|
||
assert.deepStrictEqual(fc.args, { q: "test" });
|
||
});
|
||
|
||
test("xaiCompletedToGeminiJson: maps usage to usageMetadata", () => {
|
||
const completed = {
|
||
model: "grok-4",
|
||
output: [],
|
||
usage: { input_tokens: 10, output_tokens: 20 },
|
||
};
|
||
const result = xaiCompletedToGeminiJson(completed) as Record<string, unknown>;
|
||
const meta = result.usageMetadata as Record<string, unknown>;
|
||
assert.equal(meta.promptTokenCount, 10);
|
||
assert.equal(meta.candidatesTokenCount, 20);
|
||
assert.equal(meta.totalTokenCount, 30);
|
||
});
|