Files
OmniRoute/tests/unit/xai-translators.test.ts
Diego Rodrigues de Sa e Souza dc40911583 Release v3.8.39 (#5164)
* 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>
2026-06-28 06:58:29 -03:00

516 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 13999 to low", () => {
assert.equal(budgetToEffort(1), "low");
assert.equal(budgetToEffort(3999), "low");
});
test("budgetToEffort: maps 400015999 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);
});