Files
pi_harness/packages/ai/test/openai-completions-cache-control-format.test.ts
T
Mario Zechner 8a0903ebf2 feat(ai): compat entrypoint, core-only root barrel (phase 5)
The root barrel is now core-only and side-effect free: types,
createModels/createProvider, auth substrate, lazyStream/lazyApi, faux,
utils. Generated catalogs, api-registry, env-api-keys, images, global
stream functions, and per-API lazy wrappers leave the root.

New @earendil-works/pi-ai/compat preserves the old surface verbatim as
a strict superset of the root: api-dispatch stream/complete with env
key injection, the builtin registration side effect (skip-if-present so
it cannot clobber earlier overrides), deprecated getModel/getModels/
getProviders aliases of the new getBuiltin* reads in providers/all,
lazy api wrappers + setBedrockProviderModule, and image generation.
Compat dies with the coding-agent ModelManager migration.

Packaging: exports map gains ./compat, ./providers/*, ./api/*;
sideEffects array lists only the effectful modules.

Old-global imports across agent/coding-agent/examples and pi-ai tests
switch to /compat (path-only; compat is a superset). The coding-agent
extension loader resolves the pi-ai ROOT specifier to compat, so
existing user extensions using the old global API keep working at
runtime until compat is removed. vitest configs alias /compat to src;
browser smoke imports old globals from /compat.
2026-06-10 21:17:12 +02:00

190 lines
5.0 KiB
TypeScript

import { Type } from "typebox";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import { getModel } from "../src/compat.ts";
import type { Model } from "../src/types.ts";
interface CacheControl {
type: "ephemeral";
ttl?: string;
}
interface TextPart {
type: "text";
text: string;
cache_control?: CacheControl;
}
interface ToolWithCacheControl {
type: string;
cache_control?: CacheControl;
}
interface CapturedParams {
messages: Array<{
role: string;
content: string | TextPart[] | null;
}>;
tools?: ToolWithCacheControl[];
}
const mockState = vi.hoisted(() => ({
lastParams: undefined as CapturedParams | undefined,
}));
vi.mock("openai", () => {
class FakeOpenAI {
chat = {
completions: {
create: (params: CapturedParams) => {
mockState.lastParams = params;
const stream = {
async *[Symbol.asyncIterator]() {
yield {
id: "chatcmpl-test",
choices: [{ delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
};
},
};
const promise = Promise.resolve(stream) as Promise<typeof stream> & {
withResponse: () => Promise<{
data: typeof stream;
response: { status: number; headers: Headers };
}>;
};
promise.withResponse = async () => ({
data: stream,
response: { status: 200, headers: new Headers() },
});
return promise;
},
},
};
}
return { default: FakeOpenAI };
});
async function capturePayload(
model: Model<"openai-completions">,
options?: { cacheRetention?: "none" | "short" | "long" },
): Promise<CapturedParams> {
const timestamp = Date.now();
await streamOpenAICompletions(
model,
{
systemPrompt: "System prompt",
messages: [{ role: "user", content: "Hello", timestamp }],
tools: [
{
name: "read",
description: "Read a file",
parameters: Type.Object({
path: Type.String(),
}),
},
],
},
{ apiKey: "test-key", ...options },
).result();
if (!mockState.lastParams) {
throw new Error("Expected payload to be captured");
}
return mockState.lastParams;
}
function getInstructionMessage(params: CapturedParams) {
return params.messages.find((message) => message.role === "system" || message.role === "developer");
}
function expectAnthropicCacheMarkers(params: CapturedParams): void {
const instructionMessage = getInstructionMessage(params);
expect(instructionMessage).toBeDefined();
expect(Array.isArray(instructionMessage?.content)).toBe(true);
expect((instructionMessage?.content as TextPart[])[0]?.cache_control).toEqual({ type: "ephemeral" });
expect(params.tools).toHaveLength(1);
expect(params.tools?.[0]?.cache_control).toEqual({ type: "ephemeral" });
const lastMessage = params.messages[params.messages.length - 1];
expect(lastMessage.role).toBe("user");
expect(Array.isArray(lastMessage.content)).toBe(true);
expect((lastMessage.content as TextPart[])[0]?.cache_control).toEqual({ type: "ephemeral" });
}
describe("openai-completions cacheControlFormat", () => {
beforeEach(() => {
mockState.lastParams = undefined;
});
it("applies Anthropic-style cache markers when model compat enables them", async () => {
const model: Model<"openai-completions"> = {
id: "custom-qwen",
name: "Custom Qwen",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://example.com/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32000,
compat: {
cacheControlFormat: "anthropic",
},
};
const params = await capturePayload(model);
expectAnthropicCacheMarkers(params);
});
it("preserves Anthropic-style cache markers for OpenRouter Anthropic models", async () => {
const model = getModel("openrouter", "anthropic/claude-sonnet-4");
const params = await capturePayload(model);
expectAnthropicCacheMarkers(params);
});
it("omits Anthropic-style cache markers when cacheRetention is none", async () => {
const model: Model<"openai-completions"> = {
id: "custom-qwen",
name: "Custom Qwen",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://example.com/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32000,
compat: {
cacheControlFormat: "anthropic",
},
};
const params = await capturePayload(model, { cacheRetention: "none" });
const instructionMessage = getInstructionMessage(params);
expect(Array.isArray(instructionMessage?.content)).toBe(false);
expect(params.tools?.[0]?.cache_control).toBeUndefined();
expect(typeof params.messages[params.messages.length - 1]?.content).toBe("string");
});
});