65 lines
4.3 KiB
TypeScript
65 lines
4.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createZcodeModelGateway } from "@/lib/ai/zcode-agent/model-gateway";
|
|
import type { AgentModelEntry, AgentCircuitStore } from "@/lib/ai/runtime/agent-model-failover";
|
|
|
|
const body = { model: "primary", stream: true, messages: [{ role: "user", content: "request" }], tools: [{ type: "function", function: { name: "readCanvas", parameters: { type: "object" } } }] };
|
|
function entry(id: string, responses = false): AgentModelEntry {
|
|
return { model: { id, modelId: id, capabilities: responses ? { vip98k: { invocation: { path: "/v1/responses", headers: { "X-Relay-Route-Option": "route-proof" } } } } : null }, provider: { type: responses ? "vip98k" : "openai", baseUrl: "https://fixture.invalid", apiKey: "synthetic-only" } } as AgentModelEntry;
|
|
}
|
|
function fixture() {
|
|
const paused = new Set<string>(); const calls: Array<{ url: string; request: any }> = [];
|
|
const circuit: AgentCircuitStore = { isPaused: async e => paused.has(e.model.id), fail: async e => { paused.add(e.model.id); } };
|
|
return { paused, calls, circuit };
|
|
}
|
|
|
|
test("a broken primary and a Responses-only backup work through one stable native protocol", async () => {
|
|
const s = fixture(); const primary = entry("primary"); const backup = entry("backup", true);
|
|
const original = global.fetch;
|
|
global.fetch = async (url, init) => {
|
|
const request = JSON.parse(String(init?.body)); s.calls.push({ url: String(url), request });
|
|
if (request.model === "primary") return new Response("invalid authentication", { status: 401 });
|
|
assert.equal(request.stream, false); assert.equal(request.tools[0].name, "readCanvas");
|
|
assert.equal((init?.headers as Record<string, string>)["X-Relay-Route-Option"], "route-proof");
|
|
return Response.json({ status: "completed", output: [{ type: "function_call", call_id: "once", name: "readCanvas", arguments: "{}" }] });
|
|
};
|
|
try {
|
|
const gateway = await createZcodeModelGateway(primary, undefined, [primary, backup], s.circuit);
|
|
assert.equal(gateway.apiType, "openai-chat-completions");
|
|
const response = await gateway.forward(body, "/v1/chat/completions", new AbortController().signal);
|
|
assert.equal(response.status, 200); assert.match(await response.text(), /"id":"once"/);
|
|
assert.deepEqual([...s.paused], ["primary"]);
|
|
const nextGateway = await createZcodeModelGateway(primary, undefined, [primary, backup], s.circuit);
|
|
await nextGateway.forward(body, "/v1/chat/completions", new AbortController().signal);
|
|
assert.deepEqual(s.calls.map(x => x.request.model), ["primary", "backup", "backup"]);
|
|
assert.equal(s.calls[1].url, "https://fixture.invalid/v1/responses");
|
|
assert.deepEqual(body.messages, [{ role: "user", content: "request" }]);
|
|
} finally { global.fetch = original; }
|
|
});
|
|
|
|
test("truncated tool output cannot escape before fallback supplies a complete replacement", async () => {
|
|
const s = fixture(); const primary = entry("primary"); const backup = entry("backup");
|
|
const original = global.fetch;
|
|
global.fetch = async (_url, init) => {
|
|
const request = JSON.parse(String(init?.body)); s.calls.push({ url: "", request });
|
|
return Response.json({ choices: [{ finish_reason: "tool_calls", message: { tool_calls: [{ id: "once", type: "function", function: { name: "readCanvas", arguments: request.model === "primary" ? "{bad" : "{}" } }] } }] });
|
|
};
|
|
try {
|
|
const gateway = await createZcodeModelGateway(primary, undefined, [primary, backup], s.circuit);
|
|
const response = await gateway.forward(body, "/v1/chat/completions", new AbortController().signal);
|
|
const output = await response.text();
|
|
assert.equal(response.status, 200); assert(!output.includes("{bad"));
|
|
assert.deepEqual(s.calls.map(x => x.request.model), ["primary", "backup"]);
|
|
assert.deepEqual([...s.paused], ["primary"]);
|
|
} finally { global.fetch = original; }
|
|
});
|
|
|
|
test("already paused models receive no HTTP call even in a new gateway", async () => {
|
|
const s = fixture(); s.paused.add("primary");
|
|
const original = global.fetch; global.fetch = async () => { throw new Error("must not contact paused model"); };
|
|
try {
|
|
const gateway = await createZcodeModelGateway(entry("primary"), undefined, [entry("primary")], s.circuit);
|
|
assert.equal((await gateway.forward(body, "/v1/chat/completions", new AbortController().signal)).status, 503);
|
|
} finally { global.fetch = original; }
|
|
});
|