summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/stream.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/openai-stream/src/stream.test.ts')
-rw-r--r--packages/openai-stream/src/stream.test.ts1652
1 files changed, 826 insertions, 826 deletions
diff --git a/packages/openai-stream/src/stream.test.ts b/packages/openai-stream/src/stream.test.ts
index 0650153..ea8a079 100644
--- a/packages/openai-stream/src/stream.test.ts
+++ b/packages/openai-stream/src/stream.test.ts
@@ -5,860 +5,860 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type StreamConfig, streamChat } from "./stream.js";
async function collectEvents(iter: AsyncIterable<ProviderEvent>): Promise<ProviderEvent[]> {
- const events: ProviderEvent[] = [];
- for await (const event of iter) {
- events.push(event);
- }
- return events;
+ const events: ProviderEvent[] = [];
+ for await (const event of iter) {
+ events.push(event);
+ }
+ return events;
}
function assertDefined<T>(v: T, msg?: string): asserts v is NonNullable<T> {
- if (v === undefined || v === null) {
- throw new Error(msg ?? "expected defined");
- }
+ if (v === undefined || v === null) {
+ throw new Error(msg ?? "expected defined");
+ }
}
interface CapturedSpan {
- name: string;
- attrs: Record<string, string | number | boolean | null>;
- body?: string | undefined;
- endOutcome?:
- | { err?: unknown; attrs?: Record<string, string | number | boolean | null> }
- | undefined;
+ name: string;
+ attrs: Record<string, string | number | boolean | null>;
+ body?: string | undefined;
+ endOutcome?:
+ | { err?: unknown; attrs?: Record<string, string | number | boolean | null> }
+ | undefined;
}
function createFakeLogger(): { logger: Logger; spans: CapturedSpan[] } {
- const spans: CapturedSpan[] = [];
- let spanAttrBuffer: Record<string, string | number | boolean | null> = {};
- let spanBodyBuffer: string | undefined;
-
- const fakeSpan: Span = {
- id: "fake-span-id",
- log: {} as Logger,
- setAttributes(attrs) {
- Object.assign(spanAttrBuffer, attrs);
- },
- addLink() {},
- child() {
- return fakeSpan;
- },
- end(outcome?) {
- spans.push({
- name: "provider.request",
- attrs: { ...spanAttrBuffer },
- body: spanBodyBuffer,
- endOutcome: outcome as CapturedSpan["endOutcome"],
- });
- },
- };
-
- const logger: Logger = {
- debug() {},
- info() {},
- warn() {},
- error() {},
- child() {
- return logger;
- },
- span(_name, attrs, body) {
- spanAttrBuffer = attrs ? { ...attrs } : {};
- spanBodyBuffer = body;
- return fakeSpan;
- },
- };
-
- return { logger, spans };
+ const spans: CapturedSpan[] = [];
+ let spanAttrBuffer: Record<string, string | number | boolean | null> = {};
+ let spanBodyBuffer: string | undefined;
+
+ const fakeSpan: Span = {
+ id: "fake-span-id",
+ log: {} as Logger,
+ setAttributes(attrs) {
+ Object.assign(spanAttrBuffer, attrs);
+ },
+ addLink() {},
+ child() {
+ return fakeSpan;
+ },
+ end(outcome?) {
+ spans.push({
+ name: "provider.request",
+ attrs: { ...spanAttrBuffer },
+ body: spanBodyBuffer,
+ endOutcome: outcome as CapturedSpan["endOutcome"],
+ });
+ },
+ };
+
+ const logger: Logger = {
+ debug() {},
+ info() {},
+ warn() {},
+ error() {},
+ child() {
+ return logger;
+ },
+ span(_name, attrs, body) {
+ spanAttrBuffer = attrs ? { ...attrs } : {};
+ spanBodyBuffer = body;
+ return fakeSpan;
+ },
+ };
+
+ return { logger, spans };
}
function makeConfig(apiKey = "sk-test-1234567890abcdef"): StreamConfig {
- return {
- baseURL: "https://api.example.com/v1",
- apiKey,
- model: "test-model",
- };
+ return {
+ baseURL: "https://api.example.com/v1",
+ apiKey,
+ model: "test-model",
+ };
}
function mockFetch(handler: (url: string | URL | Request, init?: RequestInit) => unknown): void {
- globalThis.fetch = vi.fn(handler) as unknown as typeof globalThis.fetch;
+ globalThis.fetch = vi.fn(handler) as unknown as typeof globalThis.fetch;
}
function makeMessages(): readonly ChatMessage[] {
- return [
- {
- role: "user",
- chunks: [{ type: "text", text: "Hello" }],
- },
- ];
+ return [
+ {
+ role: "user",
+ chunks: [{ type: "text", text: "Hello" }],
+ },
+ ];
}
function sseBody(...lines: string[]): ReadableStream<Uint8Array> {
- const encoder = new TextEncoder();
- const chunks = lines.map((l) => encoder.encode(`${l}\n`));
- let index = 0;
- return new ReadableStream<Uint8Array>({
- pull(controller) {
- if (index < chunks.length) {
- const chunk = chunks[index];
- assertDefined(chunk);
- controller.enqueue(chunk);
- index++;
- } else {
- controller.close();
- }
- },
- });
+ const encoder = new TextEncoder();
+ const chunks = lines.map((l) => encoder.encode(`${l}\n`));
+ let index = 0;
+ return new ReadableStream<Uint8Array>({
+ pull(controller) {
+ if (index < chunks.length) {
+ const chunk = chunks[index];
+ assertDefined(chunk);
+ controller.enqueue(chunk);
+ index++;
+ } else {
+ controller.close();
+ }
+ },
+ });
}
describe("streamChat — provider.request AFTER capture", () => {
- let originalFetch: typeof globalThis.fetch;
-
- beforeEach(() => {
- originalFetch = globalThis.fetch;
- });
-
- afterEach(() => {
- globalThis.fetch = originalFetch;
- });
-
- it("opens a provider.request span with verbatim request body", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
- 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- expect(spans).toHaveLength(1);
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.name).toBe("provider.request");
- expect(span.attrs["request.method"]).toBe("POST");
- expect(span.attrs["request.body"]).toBeUndefined();
-
- assertDefined(span.body);
- const capturedBody = JSON.parse(span.body);
- expect(capturedBody.model).toBe("test-model");
- expect(capturedBody.stream).toBe(true);
- expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello" }]);
-
- expect(span.endOutcome?.attrs?.status).toBe(200);
- });
-
- it("redacts a long API key (≥13 chars → reveal 3 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcdefghijkmnop");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-2","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-2","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer sk-…redacted…nop");
- expect(authHeader).not.toContain("abcdefghijkm");
- });
-
- it("redacts a medium API key (8–10 chars → reveal 1 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcde");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-3","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-3","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer s…redacted…e");
- });
-
- it("redacts a short API key (≤7 chars → full mask)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("secret!");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-4","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer …redacted…");
- });
-
- it("captures cache tokens from the response", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-5","choices":[{"delta":{"content":"Hi"},"index":0}]}',
- 'data: {"id":"cmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- 'data: {"id":"cmpl-5","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":80,"cache_write_tokens":10}}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.["usage.inputTokens"]).toBe(100);
- expect(span.endOutcome?.attrs?.["usage.outputTokens"]).toBe(20);
- expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(80);
- expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBe(10);
- });
-
- it("captures cache_read_tokens alone", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-6","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- 'data: {"id":"cmpl-6","usage":{"prompt_tokens":50,"completion_tokens":5,"cache_read_tokens":45}}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(45);
- expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBeUndefined();
- });
-
- it("records HTTP error status and error body without throwing", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response("Invalid request body", {
- status: 400,
- headers: { "Content-Type": "text/plain" },
- }),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "HTTP 400: Invalid request body",
- code: "400",
- retryable: false,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.status).toBe(400);
- expect(span.endOutcome?.attrs?.["response.error_body"]).toBe("Invalid request body");
- expect(span.endOutcome?.err).toBeInstanceOf(Error);
- });
-
- it("records network error without throwing", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(() => {
- throw new Error("connection refused");
- });
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "connection refused",
- retryable: true,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.err).toBeInstanceOf(Error);
- expect((span.endOutcome?.err as Error).message).toBe("connection refused");
- });
-
- it("detects cache_control breakpoint absence in a normal request body", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-7","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-7","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs["request.cache_control_present"]).toBe(false);
- });
-
- it("does not open a span when opts.logger is absent", async () => {
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-8","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-8","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), []));
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- });
-
- it("fail-safe: logger throwing does not break stream()", async () => {
- const brokenLogger: Logger = {
- debug() {},
- info() {},
- warn() {},
- error() {},
- child() {
- return brokenLogger;
- },
- span() {
- throw new Error("logger exploded");
- },
- };
-
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-9","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-9","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(
- streamChat(config, makeMessages(), [], { logger: brokenLogger }),
- );
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- expect(events.some((e) => e.type === "finish")).toBe(true);
- });
-
- it("redacts an 11-char API key (reveal 2 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcde1234");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-10","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-10","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer sk…redacted…34");
- });
-
- it("records server error (500) as retryable", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response("Internal Server Error", {
- status: 500,
- headers: { "Content-Type": "text/plain" },
- }),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "HTTP 500: Internal Server Error",
- code: "500",
- retryable: true,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- expect(spans[0].endOutcome?.attrs?.status).toBe(500);
- });
-
- it("captures model and url on the span", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-11","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-11","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs.model).toBe("test-model");
- expect(span.attrs.url).toBe("https://api.example.com/v1/chat/completions");
- });
-
- it("uses opts.model override in capture", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-12","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-12","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(
- streamChat(config, makeMessages(), [], { logger, model: "override-model" }),
- );
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs.model).toBe("override-model");
-
- assertDefined(span.body);
- const capturedBody = JSON.parse(span.body);
- expect(capturedBody.model).toBe("override-model");
- });
+ let originalFetch: typeof globalThis.fetch;
+
+ beforeEach(() => {
+ originalFetch = globalThis.fetch;
+ });
+
+ afterEach(() => {
+ globalThis.fetch = originalFetch;
+ });
+
+ it("opens a provider.request span with verbatim request body", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
+ 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ expect(spans).toHaveLength(1);
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.name).toBe("provider.request");
+ expect(span.attrs["request.method"]).toBe("POST");
+ expect(span.attrs["request.body"]).toBeUndefined();
+
+ assertDefined(span.body);
+ const capturedBody = JSON.parse(span.body);
+ expect(capturedBody.model).toBe("test-model");
+ expect(capturedBody.stream).toBe(true);
+ expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello" }]);
+
+ expect(span.endOutcome?.attrs?.status).toBe(200);
+ });
+
+ it("redacts a long API key (≥13 chars → reveal 3 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcdefghijkmnop");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-2","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-2","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer sk-…redacted…nop");
+ expect(authHeader).not.toContain("abcdefghijkm");
+ });
+
+ it("redacts a medium API key (8–10 chars → reveal 1 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcde");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-3","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-3","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer s…redacted…e");
+ });
+
+ it("redacts a short API key (≤7 chars → full mask)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("secret!");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-4","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer …redacted…");
+ });
+
+ it("captures cache tokens from the response", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-5","choices":[{"delta":{"content":"Hi"},"index":0}]}',
+ 'data: {"id":"cmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ 'data: {"id":"cmpl-5","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":80,"cache_write_tokens":10}}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.["usage.inputTokens"]).toBe(100);
+ expect(span.endOutcome?.attrs?.["usage.outputTokens"]).toBe(20);
+ expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(80);
+ expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBe(10);
+ });
+
+ it("captures cache_read_tokens alone", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-6","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ 'data: {"id":"cmpl-6","usage":{"prompt_tokens":50,"completion_tokens":5,"cache_read_tokens":45}}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(45);
+ expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBeUndefined();
+ });
+
+ it("records HTTP error status and error body without throwing", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response("Invalid request body", {
+ status: 400,
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "HTTP 400: Invalid request body",
+ code: "400",
+ retryable: false,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.status).toBe(400);
+ expect(span.endOutcome?.attrs?.["response.error_body"]).toBe("Invalid request body");
+ expect(span.endOutcome?.err).toBeInstanceOf(Error);
+ });
+
+ it("records network error without throwing", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(() => {
+ throw new Error("connection refused");
+ });
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "connection refused",
+ retryable: true,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.err).toBeInstanceOf(Error);
+ expect((span.endOutcome?.err as Error).message).toBe("connection refused");
+ });
+
+ it("detects cache_control breakpoint absence in a normal request body", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-7","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-7","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs["request.cache_control_present"]).toBe(false);
+ });
+
+ it("does not open a span when opts.logger is absent", async () => {
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-8","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-8","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), []));
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ });
+
+ it("fail-safe: logger throwing does not break stream()", async () => {
+ const brokenLogger: Logger = {
+ debug() {},
+ info() {},
+ warn() {},
+ error() {},
+ child() {
+ return brokenLogger;
+ },
+ span() {
+ throw new Error("logger exploded");
+ },
+ };
+
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-9","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-9","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(
+ streamChat(config, makeMessages(), [], { logger: brokenLogger }),
+ );
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ expect(events.some((e) => e.type === "finish")).toBe(true);
+ });
+
+ it("redacts an 11-char API key (reveal 2 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcde1234");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-10","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-10","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer sk…redacted…34");
+ });
+
+ it("records server error (500) as retryable", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response("Internal Server Error", {
+ status: 500,
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "HTTP 500: Internal Server Error",
+ code: "500",
+ retryable: true,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ expect(spans[0].endOutcome?.attrs?.status).toBe(500);
+ });
+
+ it("captures model and url on the span", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-11","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-11","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs.model).toBe("test-model");
+ expect(span.attrs.url).toBe("https://api.example.com/v1/chat/completions");
+ });
+
+ it("uses opts.model override in capture", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-12","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-12","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(
+ streamChat(config, makeMessages(), [], { logger, model: "override-model" }),
+ );
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs.model).toBe("override-model");
+
+ assertDefined(span.body);
+ const capturedBody = JSON.parse(span.body);
+ expect(capturedBody.model).toBe("override-model");
+ });
});
describe("streamChat — hermetic replay (trace-replay)", () => {
- const testDir = new URL(".", import.meta.url).pathname;
- const fixturePath = `${testDir}__fixtures__/flash-text-turn.json`;
- const toolFixturePath = `${testDir}__fixtures__/tool-call-turn.json`;
-
- it("replays a text-turn fixture and produces correct ProviderEvents", async () => {
- const fixture = loadFixture(fixturePath);
- const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 64 });
-
- const config: StreamConfig = {
- baseURL: "https://api.example.com/v1",
- apiKey: "sk-test-1234567890abcdef",
- model: "deepseek-v4-flash",
- fetchFn: replayFetchFn,
- };
-
- const messages: ChatMessage[] = [
- { role: "user", chunks: [{ type: "text", text: "Hello, how are you?" }] },
- ];
-
- const events = await collectEvents(streamChat(config, messages, []));
-
- const textDeltas = events.filter(
- (e): e is Extract<ProviderEvent, { type: "text-delta" }> => e.type === "text-delta",
- );
- const fullText = textDeltas.map((e) => e.delta).join("");
- expect(fullText).toBe("Hello there friend");
-
- const finishEvents = events.filter((e) => e.type === "finish");
- expect(finishEvents).toHaveLength(1);
- expect(finishEvents[0]).toEqual({ type: "finish", reason: "stop" });
-
- const usageEvents = events.filter(
- (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
- );
- expect(usageEvents).toHaveLength(1);
- expect(usageEvents[0]?.usage.inputTokens).toBe(665);
- expect(usageEvents[0]?.usage.outputTokens).toBe(90);
- expect(usageEvents[0]?.usage.cacheReadTokens).toBe(384);
-
- const captured = getCapturedRequest();
- assertDefined(captured);
- expect(captured.method).toBe("POST");
- expect(captured.url).toBe("https://api.example.com/v1/chat/completions");
- expect(captured.headers["Content-Type"]).toBe("application/json");
- expect(captured.headers.Authorization).toBe("Bearer sk-test-1234567890abcdef");
-
- assertDefined(captured.body);
- const capturedBody = JSON.parse(captured.body);
- expect(capturedBody.model).toBe("deepseek-v4-flash");
- expect(capturedBody.stream).toBe(true);
- expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello, how are you?" }]);
- });
-
- it("replays a tool-call-turn fixture and produces tool-call + finish events", async () => {
- const fixture = loadFixture(toolFixturePath);
- const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 48 });
-
- const config: StreamConfig = {
- baseURL: "https://api.example.com/v1",
- apiKey: "sk-test-1234567890abcdef",
- model: "deepseek-v4-flash",
- fetchFn: replayFetchFn,
- };
-
- const messages: ChatMessage[] = [
- { role: "user", chunks: [{ type: "text", text: "What is the weather in Tokyo?" }] },
- ];
-
- const weatherTool = {
- name: "get_weather",
- description: "Get current weather for a location",
- parameters: {
- type: "object" as const,
- properties: { location: { type: "string" as const } },
- required: ["location"],
- },
- execute: async () => ({ content: "" }),
- };
-
- const events = await collectEvents(streamChat(config, messages, [weatherTool]));
-
- const toolCalls = events.filter(
- (e): e is Extract<ProviderEvent, { type: "tool-call" }> => e.type === "tool-call",
- );
- expect(toolCalls).toHaveLength(1);
- expect(toolCalls[0]?.toolCallId).toBe("call_abc123");
- expect(toolCalls[0]?.toolName).toBe("get_weather");
- expect(toolCalls[0]?.input).toEqual({ location: "Tokyo" });
-
- const finishEvents = events.filter((e) => e.type === "finish");
- expect(finishEvents).toHaveLength(1);
- expect(finishEvents[0]).toEqual({ type: "finish", reason: "tool_calls" });
-
- const usageEvents = events.filter(
- (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
- );
- expect(usageEvents).toHaveLength(1);
- expect(usageEvents[0]?.usage.inputTokens).toBe(45);
- expect(usageEvents[0]?.usage.outputTokens).toBe(12);
- expect(usageEvents[0]?.usage.cacheReadTokens).toBe(30);
- expect(usageEvents[0]?.usage.cacheWriteTokens).toBe(5);
-
- const captured = getCapturedRequest();
- assertDefined(captured);
- expect(captured.method).toBe("POST");
- assertDefined(captured.body);
- const capturedBody = JSON.parse(captured.body);
- expect(capturedBody.tools).toHaveLength(1);
- expect(capturedBody.tools[0].function.name).toBe("get_weather");
- });
+ const testDir = new URL(".", import.meta.url).pathname;
+ const fixturePath = `${testDir}__fixtures__/flash-text-turn.json`;
+ const toolFixturePath = `${testDir}__fixtures__/tool-call-turn.json`;
+
+ it("replays a text-turn fixture and produces correct ProviderEvents", async () => {
+ const fixture = loadFixture(fixturePath);
+ const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 64 });
+
+ const config: StreamConfig = {
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test-1234567890abcdef",
+ model: "deepseek-v4-flash",
+ fetchFn: replayFetchFn,
+ };
+
+ const messages: ChatMessage[] = [
+ { role: "user", chunks: [{ type: "text", text: "Hello, how are you?" }] },
+ ];
+
+ const events = await collectEvents(streamChat(config, messages, []));
+
+ const textDeltas = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "text-delta" }> => e.type === "text-delta",
+ );
+ const fullText = textDeltas.map((e) => e.delta).join("");
+ expect(fullText).toBe("Hello there friend");
+
+ const finishEvents = events.filter((e) => e.type === "finish");
+ expect(finishEvents).toHaveLength(1);
+ expect(finishEvents[0]).toEqual({ type: "finish", reason: "stop" });
+
+ const usageEvents = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
+ );
+ expect(usageEvents).toHaveLength(1);
+ expect(usageEvents[0]?.usage.inputTokens).toBe(665);
+ expect(usageEvents[0]?.usage.outputTokens).toBe(90);
+ expect(usageEvents[0]?.usage.cacheReadTokens).toBe(384);
+
+ const captured = getCapturedRequest();
+ assertDefined(captured);
+ expect(captured.method).toBe("POST");
+ expect(captured.url).toBe("https://api.example.com/v1/chat/completions");
+ expect(captured.headers["Content-Type"]).toBe("application/json");
+ expect(captured.headers.Authorization).toBe("Bearer sk-test-1234567890abcdef");
+
+ assertDefined(captured.body);
+ const capturedBody = JSON.parse(captured.body);
+ expect(capturedBody.model).toBe("deepseek-v4-flash");
+ expect(capturedBody.stream).toBe(true);
+ expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello, how are you?" }]);
+ });
+
+ it("replays a tool-call-turn fixture and produces tool-call + finish events", async () => {
+ const fixture = loadFixture(toolFixturePath);
+ const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 48 });
+
+ const config: StreamConfig = {
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test-1234567890abcdef",
+ model: "deepseek-v4-flash",
+ fetchFn: replayFetchFn,
+ };
+
+ const messages: ChatMessage[] = [
+ { role: "user", chunks: [{ type: "text", text: "What is the weather in Tokyo?" }] },
+ ];
+
+ const weatherTool = {
+ name: "get_weather",
+ description: "Get current weather for a location",
+ parameters: {
+ type: "object" as const,
+ properties: { location: { type: "string" as const } },
+ required: ["location"],
+ },
+ execute: async () => ({ content: "" }),
+ };
+
+ const events = await collectEvents(streamChat(config, messages, [weatherTool]));
+
+ const toolCalls = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "tool-call" }> => e.type === "tool-call",
+ );
+ expect(toolCalls).toHaveLength(1);
+ expect(toolCalls[0]?.toolCallId).toBe("call_abc123");
+ expect(toolCalls[0]?.toolName).toBe("get_weather");
+ expect(toolCalls[0]?.input).toEqual({ location: "Tokyo" });
+
+ const finishEvents = events.filter((e) => e.type === "finish");
+ expect(finishEvents).toHaveLength(1);
+ expect(finishEvents[0]).toEqual({ type: "finish", reason: "tool_calls" });
+
+ const usageEvents = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
+ );
+ expect(usageEvents).toHaveLength(1);
+ expect(usageEvents[0]?.usage.inputTokens).toBe(45);
+ expect(usageEvents[0]?.usage.outputTokens).toBe(12);
+ expect(usageEvents[0]?.usage.cacheReadTokens).toBe(30);
+ expect(usageEvents[0]?.usage.cacheWriteTokens).toBe(5);
+
+ const captured = getCapturedRequest();
+ assertDefined(captured);
+ expect(captured.method).toBe("POST");
+ assertDefined(captured.body);
+ const capturedBody = JSON.parse(captured.body);
+ expect(capturedBody.tools).toHaveLength(1);
+ expect(capturedBody.tools[0].function.name).toBe("get_weather");
+ });
});
describe("streamChat — record-mode redaction (trace-replay)", () => {
- /**
- * Graduated secret mask — §6 tiers. Duplicated locally (isolation-over-dry).
- * ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask.
- */
- function maskSecret(value: string): string {
- const len = value.length;
- if (len <= 7) return "…redacted…";
- let reveal: number;
- if (len >= 13) {
- reveal = 3;
- } else if (len >= 11) {
- reveal = 2;
- } else {
- reveal = 1;
- }
- return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
- }
-
- it("self-redacts auth header in onExchange and produces a secret-free fixture", async () => {
- const apiKey = "sk-abcdefghijkmnop";
- const responseBody =
- 'data: {"id":"cmpl-r","choices":[{"delta":{"content":"ok"},"index":0}],"usage":{"prompt_tokens":5,"completion_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}\n\ndata: [DONE]\n';
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () =>
- new Response(responseBody, {
- status: 200,
- headers: { "content-type": "text/event-stream" },
- }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "content-type": "application/json",
- authorization: `Bearer ${apiKey}`,
- },
- body: '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}',
- });
-
- assertDefined(capturedFixture);
-
- expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
- expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
- expect(capturedFixture.request.headers["content-type"]).toBe("application/json");
-
- expect(capturedFixture.request.body).toContain('"model":"test"');
- expect(capturedFixture.request.body).toContain('"content":"hi"');
-
- expect(capturedFixture.response.status).toBe(200);
- expect(capturedFixture.response.body).toBe(responseBody);
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).toContain("Bearer sk-…redacted…nop");
- expect(serialized).not.toContain("abcdefghijkm");
- expect(serialized).toContain("content");
- expect(serialized).toContain("hi");
- });
-
- it("redacts capitalized Authorization header (the real leak casing)", async () => {
- const apiKey = "sk-LIVEKEY1234567890abcdef";
- const responseBody = "data: [DONE]\n";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () =>
- new Response(responseBody, {
- status: 200,
- headers: { "content-type": "text/event-stream" },
- }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${apiKey}`,
- },
- body: '{"model":"test","messages":[],"stream":true}',
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.Authorization).toBe("Bearer sk-…redacted…def");
- expect(capturedFixture.request.headers.Authorization).not.toContain("LIVEKEY1234567890abc");
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).not.toContain("LIVEKEY1234567890abc");
- expect(serialized).toContain("Bearer sk-…redacted…def");
- });
-
- it("redacts lowercase authorization header", async () => {
- const apiKey = "sk-abcdefghijkmnop";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { authorization: `Bearer ${apiKey}` },
- body: null,
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
- expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
- });
-
- it("guard: no header named authorization (any case) survives with a raw sk- token", async () => {
- const apiKey = "sk-REALKEY_1234567890abcdef";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { Authorization: `Bearer ${apiKey}` },
- body: null,
- });
-
- assertDefined(capturedFixture);
- for (const [key, value] of Object.entries(capturedFixture.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- expect(value).not.toContain(apiKey);
- expect(value).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
- }
- }
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).not.toContain(apiKey);
- expect(serialized).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
- });
-
- it("redacts a short API key (≤7 chars → full mask)", async () => {
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { authorization: "Bearer secret!" },
- body: null,
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.authorization).toBe("Bearer …redacted…");
- });
+ /**
+ * Graduated secret mask — §6 tiers. Duplicated locally (isolation-over-dry).
+ * ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask.
+ */
+ function maskSecret(value: string): string {
+ const len = value.length;
+ if (len <= 7) return "…redacted…";
+ let reveal: number;
+ if (len >= 13) {
+ reveal = 3;
+ } else if (len >= 11) {
+ reveal = 2;
+ } else {
+ reveal = 1;
+ }
+ return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
+ }
+
+ it("self-redacts auth header in onExchange and produces a secret-free fixture", async () => {
+ const apiKey = "sk-abcdefghijkmnop";
+ const responseBody =
+ 'data: {"id":"cmpl-r","choices":[{"delta":{"content":"ok"},"index":0}],"usage":{"prompt_tokens":5,"completion_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}\n\ndata: [DONE]\n';
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () =>
+ new Response(responseBody, {
+ status: 200,
+ headers: { "content-type": "text/event-stream" },
+ }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ authorization: `Bearer ${apiKey}`,
+ },
+ body: '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}',
+ });
+
+ assertDefined(capturedFixture);
+
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
+ expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
+ expect(capturedFixture.request.headers["content-type"]).toBe("application/json");
+
+ expect(capturedFixture.request.body).toContain('"model":"test"');
+ expect(capturedFixture.request.body).toContain('"content":"hi"');
+
+ expect(capturedFixture.response.status).toBe(200);
+ expect(capturedFixture.response.body).toBe(responseBody);
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).toContain("Bearer sk-…redacted…nop");
+ expect(serialized).not.toContain("abcdefghijkm");
+ expect(serialized).toContain("content");
+ expect(serialized).toContain("hi");
+ });
+
+ it("redacts capitalized Authorization header (the real leak casing)", async () => {
+ const apiKey = "sk-LIVEKEY1234567890abcdef";
+ const responseBody = "data: [DONE]\n";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () =>
+ new Response(responseBody, {
+ status: 200,
+ headers: { "content-type": "text/event-stream" },
+ }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: '{"model":"test","messages":[],"stream":true}',
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.Authorization).toBe("Bearer sk-…redacted…def");
+ expect(capturedFixture.request.headers.Authorization).not.toContain("LIVEKEY1234567890abc");
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).not.toContain("LIVEKEY1234567890abc");
+ expect(serialized).toContain("Bearer sk-…redacted…def");
+ });
+
+ it("redacts lowercase authorization header", async () => {
+ const apiKey = "sk-abcdefghijkmnop";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { authorization: `Bearer ${apiKey}` },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
+ expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
+ });
+
+ it("guard: no header named authorization (any case) survives with a raw sk- token", async () => {
+ const apiKey = "sk-REALKEY_1234567890abcdef";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { Authorization: `Bearer ${apiKey}` },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ for (const [key, value] of Object.entries(capturedFixture.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ expect(value).not.toContain(apiKey);
+ expect(value).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
+ }
+ }
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).not.toContain(apiKey);
+ expect(serialized).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
+ });
+
+ it("redacts a short API key (≤7 chars → full mask)", async () => {
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { authorization: "Bearer secret!" },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer …redacted…");
+ });
});