Files
OmniRoute/tests/unit/cliproxyapi-executor.test.ts
OmniRoute Ops f946f6e0a7 fix(cliproxyapi): conditional thinking strip to preserve valid Anthropic shape
Follow-up after testing the Capy BYOK flow. The unconditional
`delete transformed.thinking` was killing legitimate thinking configs
that applyThinkingBudget had already converted to Anthropic-valid form
({type:"enabled"|"disabled", budget_tokens:N}). Replace with a
conditional strip that:

- Preserves Anthropic-valid shapes (enabled/disabled + numeric
  budget_tokens, no extra fields)
- Strips Capy/Anthropic-SDK shapes Anthropic doesn't accept
  (type:"adaptive", or presence of the Capy-specific `display` field)

Symptom we observed: clients sending
`thinking: {type:"adaptive", display:"summarized"}` on /v1/messages
saw plain text responses with no thinking block. Their UIs (e.g. Capy)
fell back to rendering answer text inside the empty "Thought" section.

With applyThinkingBudget in adaptive mode (default for many user
configs), the body reaching this executor already has a valid
Anthropic shape — the unconditional strip was undoing that work.
Bodies that bypass applyThinkingBudget (passthrough mode) still get
stripped here because Anthropic 400s on the unconverted shape.

5 new test cases cover the preserve/strip matrix on Anthropic-shape
and OpenAI-shape bodies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:00:54 +00:00

292 lines
10 KiB
TypeScript

import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
const originalFetch = globalThis.fetch;
const originalEnv = { ...process.env };
afterEach(() => {
globalThis.fetch = originalFetch;
process.env.CLIPROXYAPI_HOST = originalEnv.CLIPROXYAPI_HOST;
process.env.CLIPROXYAPI_PORT = originalEnv.CLIPROXYAPI_PORT;
});
describe("CliproxyapiExecutor", () => {
let CliproxyapiExecutor;
beforeEach(async () => {
process.env.CLIPROXYAPI_HOST = "";
process.env.CLIPROXYAPI_PORT = "";
const mod = await import("../../open-sse/executors/cliproxyapi.ts");
CliproxyapiExecutor = mod.CliproxyapiExecutor;
});
describe("constructor", () => {
it("should default to 127.0.0.1:8317", () => {
const exec = new CliproxyapiExecutor();
assert.equal(exec.getProvider(), "cliproxyapi");
});
it("should respect CLIPROXYAPI_HOST env", () => {
process.env.CLIPROXYAPI_HOST = "192.168.1.1";
const exec = new CliproxyapiExecutor();
assert.equal(exec.getProvider(), "cliproxyapi");
});
it("should respect CLIPROXYAPI_PORT env", () => {
process.env.CLIPROXYAPI_PORT = "9999";
const exec = new CliproxyapiExecutor();
assert.equal(exec.getProvider(), "cliproxyapi");
});
});
describe("buildUrl", () => {
it("should always return /v1/chat/completions", () => {
process.env.CLIPROXYAPI_HOST = "127.0.0.1";
process.env.CLIPROXYAPI_PORT = "8317";
const exec = new CliproxyapiExecutor();
const url = exec.buildUrl("any-model", true);
assert.equal(url, "http://127.0.0.1:8317/v1/chat/completions");
});
it("should ignore model parameter", () => {
const exec = new CliproxyapiExecutor();
const url = exec.buildUrl("gpt-4", false);
assert.equal(url, "http://127.0.0.1:8317/v1/chat/completions");
});
it("should use custom host/port", () => {
process.env.CLIPROXYAPI_HOST = "10.0.0.1";
process.env.CLIPROXYAPI_PORT = "9090";
const exec = new CliproxyapiExecutor();
const url = exec.buildUrl("model", true);
assert.equal(url, "http://10.0.0.1:9090/v1/chat/completions");
});
});
describe("buildHeaders", () => {
it("should return content-type without auth when no credentials", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({});
assert.equal(headers["Content-Type"], "application/json");
assert.equal(headers["Authorization"], undefined);
});
it("should add Authorization with apiKey", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({ apiKey: "test-key" });
assert.equal(headers["Authorization"], "Bearer test-key");
});
it("should add Authorization with accessToken", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({ accessToken: "test-token" });
assert.equal(headers["Authorization"], "Bearer test-token");
});
it("should prefer apiKey over accessToken", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({ apiKey: "key", accessToken: "token" });
assert.equal(headers["Authorization"], "Bearer key");
});
it("should add Accept header for streaming", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({}, true);
assert.equal(headers["Accept"], "text/event-stream");
});
it("should not add Accept header for non-streaming", () => {
const exec = new CliproxyapiExecutor();
const headers = exec.buildHeaders({}, false);
assert.equal(headers["Accept"], undefined);
});
});
describe("transformRequest", () => {
it("should update model if body.model differs", () => {
const exec = new CliproxyapiExecutor();
const body = { model: "old-model", messages: [] };
const result = exec.transformRequest("new-model", body, true, {});
assert.equal(result.model, "new-model");
assert.deepEqual(result.messages, []);
});
it("should return body unchanged if model matches", () => {
const exec = new CliproxyapiExecutor();
const body = { model: "same-model", messages: [] };
const result = exec.transformRequest("same-model", body, true, {});
assert.equal(result.model, "same-model");
});
it("should handle non-object body", () => {
const exec = new CliproxyapiExecutor();
const result = exec.transformRequest("model", "not-an-object", true, {});
assert.equal(result, "not-an-object");
});
it("should handle null body", () => {
const exec = new CliproxyapiExecutor();
const result = exec.transformRequest("model", null, true, {});
assert.equal(result, null);
});
it("preserves Anthropic-valid thinking shape on /v1/messages routing", () => {
const exec = new CliproxyapiExecutor();
const body = {
model: "claude-opus-4-7",
system: [{ type: "text", text: "Be helpful" }],
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
thinking: { type: "enabled", budget_tokens: 10240 },
};
const result = exec.transformRequest("claude-opus-4-7", body, true, {});
assert.deepEqual(result.thinking, { type: "enabled", budget_tokens: 10240 });
});
it("preserves disabled thinking shape on /v1/messages routing", () => {
const exec = new CliproxyapiExecutor();
const body = {
model: "claude-opus-4-7",
system: [{ type: "text", text: "x" }],
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
thinking: { type: "disabled", budget_tokens: 0 },
};
const result = exec.transformRequest("claude-opus-4-7", body, true, {});
assert.deepEqual(result.thinking, { type: "disabled", budget_tokens: 0 });
});
it("strips Capy-style adaptive thinking on /v1/messages routing", () => {
const exec = new CliproxyapiExecutor();
const body = {
model: "claude-opus-4-7",
system: [{ type: "text", text: "x" }],
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
thinking: { type: "adaptive", display: "summarized" },
};
const result = exec.transformRequest("claude-opus-4-7", body, true, {});
assert.equal(result.thinking, undefined);
});
it("strips thinking carrying display field even with enabled type", () => {
const exec = new CliproxyapiExecutor();
const body = {
model: "claude-opus-4-7",
system: [{ type: "text", text: "x" }],
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
thinking: { type: "enabled", budget_tokens: 10240, display: "summarized" },
};
const result = exec.transformRequest("claude-opus-4-7", body, true, {});
assert.equal(result.thinking, undefined);
});
it("does not touch thinking on OpenAI-shape bodies", () => {
const exec = new CliproxyapiExecutor();
const body = {
model: "gpt-5.5",
messages: [{ role: "user", content: "hi" }],
thinking: { type: "adaptive", display: "summarized" },
};
const result = exec.transformRequest("gpt-5.5", body, true, {});
assert.deepEqual(result.thinking, { type: "adaptive", display: "summarized" });
});
});
describe("execute", () => {
it("should make fetch request with correct URL, headers, and body", async () => {
let capturedUrl, capturedOptions;
globalThis.fetch = async (url, options) => {
capturedUrl = url;
capturedOptions = options;
return { status: 200, ok: true };
};
const exec = new CliproxyapiExecutor();
const result = await exec.execute({
model: "test-model",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: {},
});
assert.equal(capturedUrl, "http://127.0.0.1:8317/v1/chat/completions");
assert.equal(capturedOptions.method, "POST");
assert.ok(capturedOptions.signal);
const parsed = JSON.parse(capturedOptions.body);
assert.equal(parsed.messages[0].content, "hi");
assert.ok(result.response);
});
it("should pass credentials to headers", async () => {
let capturedHeaders;
globalThis.fetch = async (_url, options) => {
capturedHeaders = options.headers;
return { status: 200, ok: true };
};
const exec = new CliproxyapiExecutor();
await exec.execute({
model: "test",
body: {},
stream: false,
credentials: { apiKey: "secret-key" },
});
assert.equal(capturedHeaders["Authorization"], "Bearer secret-key");
});
it("should merge upstream extra headers", async () => {
let capturedHeaders;
globalThis.fetch = async (_url, options) => {
capturedHeaders = options.headers;
return { status: 200, ok: true };
};
const exec = new CliproxyapiExecutor();
await exec.execute({
model: "test",
body: {},
stream: false,
credentials: {},
upstreamExtraHeaders: { "X-Custom": "value" },
});
assert.equal(capturedHeaders["X-Custom"], "value");
});
it("should handle rate limited response", async () => {
globalThis.fetch = async () => ({ status: 429, ok: false });
const log = { warn: (tag, msg) => {} };
let logged = false;
log.warn = () => {
logged = true;
};
const exec = new CliproxyapiExecutor();
const result = await exec.execute({
model: "test",
body: {},
stream: false,
credentials: {},
log,
});
assert.equal(result.response.status, 429);
});
it("should return url, headers, and transformedBody", async () => {
globalThis.fetch = async () => ({ status: 200, ok: true });
const exec = new CliproxyapiExecutor();
const result = await exec.execute({
model: "test",
body: { messages: [] },
stream: true,
credentials: {},
});
assert.ok(result.url);
assert.ok(result.headers);
assert.ok(result.transformedBody);
});
});
});