@yanlinglabs/winter-provider-conformance 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ import { type FakeRoute, type FakeServer, type RecordedRequest, type ScenarioResponder } from "./server.js";
2
+ export declare const FAKE_AZURE_KEY = "test-key-azure-0000";
3
+ export declare const FAKE_ENTRA_TOKEN = "test-token-entra-0000";
4
+ export interface AzureFakeOptions {
5
+ /** modelId -> scripted answer for the CLASSIC deployment path (chat completions). */
6
+ chatScenarios?: Record<string, ScenarioResponder | Response[]>;
7
+ /** modelId -> scripted answer for the PREVIEW `/openai/v1/responses` surface. */
8
+ responsesScenarios?: Record<string, ScenarioResponder | Response[]>;
9
+ /** Rows served by `/openai/models` and `/openai/v1/models`. */
10
+ models?: unknown[];
11
+ routes?: FakeRoute[];
12
+ }
13
+ /** The deployment segment a request addressed, or `undefined` for the preview surface. */
14
+ export declare function deploymentOf(recorded: RecordedRequest): string | undefined;
15
+ /** The `api-version` a request carried, or `undefined`. */
16
+ export declare function apiVersionOf(recorded: RecordedRequest): string | undefined;
17
+ /** Starts a fake serving both Azure surfaces plus their discovery endpoints. */
18
+ export declare function startAzureFake(opts: AzureFakeOptions): Promise<FakeServer>;
@@ -0,0 +1,90 @@
1
+ import { concatFrames, converseStreamEvent, converseStreamException } from "@yanlinglabs/winter-provider-runtime/testing";
2
+ import { type FakeServer, type RecordedRequest, type ScenarioResponder } from "./server.js";
3
+ /** AWS's own published example credentials. They authenticate nothing; the fake knows the secret so it can recompute. */
4
+ export declare const FAKE_ACCESS_KEY_ID = "AKIDEXAMPLE";
5
+ export declare const FAKE_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
6
+ export declare const FAKE_REGION = "us-east-1";
7
+ /** The model id a request addressed, read out of the PATH — Bedrock's own placement, unlike the OpenAI and Anthropic families' bodies. */
8
+ export declare function bedrockModelOf(recorded: RecordedRequest): string | undefined;
9
+ /** True when the request asked for the STREAMING operation. */
10
+ export declare function isStreamingPath(recorded: RecordedRequest): boolean;
11
+ export interface EventStreamResponseOptions {
12
+ status?: number;
13
+ /** Milliseconds to wait before writing each frame after the first — the slow-stream primitive. */
14
+ delayMs?: number;
15
+ /** END the stream after this many frames, WITHOUT the terminating `messageStop`: a truncated upstream. */
16
+ dropAfter?: number;
17
+ /**
18
+ * After every frame has been written, HOLD the stream open without closing it — the mid-turn
19
+ * stall. Bounded so it cannot outlive its test, and the timer is cleared on cancel: an
20
+ * unconditional `close()` against a controller the consumer already tore down throws from a bare
21
+ * timer callback, which Bun attributes to whichever test happens to be running (the spine hit
22
+ * exactly this in `stalledResponse` and fixed it the same way).
23
+ */
24
+ holdOpenMs?: number;
25
+ /**
26
+ * Split the whole body into chunks of this many bytes.
27
+ *
28
+ * The only way to prove ON THE WIRE that the decoder survives a frame boundary falling anywhere:
29
+ * a unit test can feed it byte by byte, but only a real socket proves the adapter's read loop
30
+ * feeds it correctly.
31
+ */
32
+ chunkSize?: number;
33
+ }
34
+ /**
35
+ * An `application/vnd.amazon.eventstream` response built from encoded frames.
36
+ *
37
+ * WRITES ONE FRAME PER PULL, and that is the fix for a defect this file shipped with for one test
38
+ * run: pacing was applied per CHUNK while `chunkSize` defaulted to the whole body, so `delayMs` was
39
+ * dead code and the "slow stream" wrote everything at once. `cancel-mid-stream` passed for the wrong
40
+ * reason — there was no mid-stream to cancel in. Frames are the unit a Bedrock consumer sees, so
41
+ * they are the unit this paces; `chunkSize` splits WITHIN a frame, which is the separate question of
42
+ * whether a decoder survives a frame spanning socket reads.
43
+ */
44
+ export declare function eventStreamResponse(frames: Uint8Array[], opts?: EventStreamResponseOptions): Response;
45
+ /** Bedrock's REST-JSON error shape: a bare `{"message"}` body with the code on `x-amzn-errortype`. */
46
+ export declare function bedrockError(status: number, errorType: string, message: string, headers?: Record<string, string>): Response;
47
+ /** A happy-path ConverseStream body: start, text, stop, metadata. */
48
+ export declare function textTurnFrames(text: string, usage?: {
49
+ inputTokens: number;
50
+ outputTokens: number;
51
+ }): Uint8Array[];
52
+ /** What the fake observed about one request's SIGNATURE. */
53
+ export interface RecordedSignature {
54
+ path: string;
55
+ /** The header names the client SIGNED, from the Authorization header's own `SignedHeaders=` list. */
56
+ signedHeaders: string[];
57
+ accessKeyId: string;
58
+ region: string;
59
+ service: string;
60
+ verified: boolean;
61
+ }
62
+ /**
63
+ * The fake, plus what it saw of each signature.
64
+ *
65
+ * `signatures` exists because `server.ts` REDACTS `authorization` as it records a request — correct
66
+ * for a request log, and it makes "was this header actually signed?" unaskable from `fake.requests`.
67
+ * A test that only checked the header was PRESENT would pass for an adapter that attached it after
68
+ * signing, which real AWS rejects with a 403 naming nothing useful.
69
+ */
70
+ export interface BedrockFakeServer extends FakeServer {
71
+ signatures: RecordedSignature[];
72
+ }
73
+ export interface BedrockFakeOptions {
74
+ /** modelId -> the scripted answer for `/model/<id>/converse[-stream]`. */
75
+ scenarios: Record<string, ScenarioResponder | Response[]>;
76
+ /** The answer to `GET /foundation-models`. Defaults to a two-row inventory. */
77
+ discovery?: ScenarioResponder;
78
+ /** Skips signature verification. ONLY the guard-on-the-guard test sets it, to prove the check is load-bearing. */
79
+ skipSignatureCheck?: boolean;
80
+ /** Skips the request-SHAPE checks (alternation, empty text). Used by the fixture that proves those checks are themselves load-bearing. */
81
+ skipShapeCheck?: boolean;
82
+ secretAccessKey?: string;
83
+ }
84
+ /**
85
+ * Starts the Bedrock fake.
86
+ *
87
+ * ALWAYS close it in a `finally` — `withFake` from the base does that for you.
88
+ */
89
+ export declare function startBedrockFake(opts: BedrockFakeOptions): Promise<BedrockFakeServer>;
90
+ export { converseStreamEvent, converseStreamException, concatFrames };
@@ -0,0 +1,46 @@
1
+ import { type FakeRoute, type FakeServer, type ScenarioResponder } from "./server.js";
2
+ export declare const FAKE_ACCOUNT_ID = "acct-test-0001";
3
+ export declare const FAKE_ACCESS_TOKEN = "test-token-codex-access";
4
+ export declare const FAKE_REFRESHED_ACCESS_TOKEN = "test-token-codex-access-refreshed";
5
+ export declare const FAKE_REFRESH_TOKEN = "test-token-codex-refresh";
6
+ /**
7
+ * An id token carrying a ChatGPT account id.
8
+ *
9
+ * UNSIGNED (`alg: "none"`), and that is honest rather than lazy: the adapter reads this claim as a
10
+ * LOCATOR — a Keychain account name and a request header — never as an authorization decision, and
11
+ * a fake that signed it would imply a verification step that does not exist.
12
+ */
13
+ export declare function fakeIdToken(accountId?: string): string;
14
+ export interface CodexTokenEndpointOptions {
15
+ /** Answer every exchange with this status instead of 200. */
16
+ failWith?: number;
17
+ accountId?: string;
18
+ accessToken?: string;
19
+ /** Omit the id token, which is what a real REFRESH grant usually does. */
20
+ omitIdToken?: boolean;
21
+ /** Omit the refresh token — a refresh grant that does not rotate it. */
22
+ omitRefreshToken?: boolean;
23
+ expiresIn?: number;
24
+ }
25
+ /** The `/oauth/token` route, serving both the authorization-code exchange and the refresh grant. */
26
+ export declare function codexTokenRoute(opts?: CodexTokenEndpointOptions): FakeRoute;
27
+ /** A codex fake, plus the bearers its `/responses` route actually saw — in order. */
28
+ export interface CodexFakeServer extends FakeServer {
29
+ bearers: string[];
30
+ }
31
+ export interface CodexFakeOptions {
32
+ /** modelId -> scripted answer, keyed exactly as the shared `scenarioTable` would. */
33
+ scenarios: Record<string, ScenarioResponder | Response[]>;
34
+ /** Model ids whose FIRST request must be answered 401 — the refresh probe. A later request carrying a different bearer is served the scenario. */
35
+ requireRefreshFor?: string[];
36
+ token?: CodexTokenEndpointOptions;
37
+ routes?: FakeRoute[];
38
+ unknownModel?: ScenarioResponder;
39
+ }
40
+ /**
41
+ * Starts a codex backend fake.
42
+ *
43
+ * Both path spellings are served: `/responses` (a base URL pointing straight at the fake) and
44
+ * `/backend-api/codex/responses` (a base URL mirroring the real backend's own path).
45
+ */
46
+ export declare function startCodexFake(opts: CodexFakeOptions): Promise<CodexFakeServer>;
@@ -0,0 +1,121 @@
1
+ import { type FakeRoute, type RecordedRequest, type SseFrame, type SseResponseOptions } from "./server.js";
2
+ /** One `parts` entry, in the family's own spelling. */
3
+ export type GeminiPart = {
4
+ text: string;
5
+ thought?: boolean;
6
+ thoughtSignature?: string;
7
+ } | {
8
+ functionCall: {
9
+ name: string;
10
+ args: Record<string, unknown>;
11
+ };
12
+ thoughtSignature?: string;
13
+ } | {
14
+ functionResponse: {
15
+ name: string;
16
+ response: Record<string, unknown>;
17
+ };
18
+ } | {
19
+ inlineData: {
20
+ mimeType: string;
21
+ data: string;
22
+ };
23
+ };
24
+ /** One streamed chunk. A chunk carrying `finishReason` is the COMPLETING one. */
25
+ export interface GeminiChunk {
26
+ parts?: GeminiPart[];
27
+ finishReason?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "PROHIBITED_CONTENT" | "MALFORMED_FUNCTION_CALL";
28
+ usageMetadata?: {
29
+ promptTokenCount?: number;
30
+ candidatesTokenCount?: number;
31
+ cachedContentTokenCount?: number;
32
+ thoughtsTokenCount?: number;
33
+ totalTokenCount?: number;
34
+ };
35
+ modelVersion?: string;
36
+ /** A top-level prompt block -- the family's own refusal shape, which carries no candidate at all. */
37
+ promptFeedback?: {
38
+ blockReason: string;
39
+ };
40
+ delayMs?: number;
41
+ }
42
+ export declare function geminiSseFrames(chunks: GeminiChunk[]): SseFrame[];
43
+ export declare function geminiStreamResponse(chunks: GeminiChunk[], opts?: SseResponseOptions): Response;
44
+ /** The family's error envelope: a NUMERIC `error.code` with the machine-readable value in `error.status`. */
45
+ export declare function geminiError(status: number, googleStatus: string, message?: string, headers?: Record<string, string>): Response;
46
+ /**
47
+ * Reads the model id out of a Gemini request.
48
+ *
49
+ * IT IS IN THE PATH, not the body -- `/v1beta/models/<model>:streamGenerateContent` -- which is why
50
+ * the base's `scenarioTable` takes a family-specific `modelOf` instead of guessing.
51
+ */
52
+ export declare function geminiModelOf(recorded: RecordedRequest): string | undefined;
53
+ export declare function geminiBody(recorded: RecordedRequest): Record<string, unknown>;
54
+ /** The `contents` array of a recorded request. */
55
+ export declare function geminiContents(recorded: RecordedRequest): Array<{
56
+ role?: string;
57
+ parts?: GeminiPart[];
58
+ }>;
59
+ export interface GeminiRequestExpectation {
60
+ model?: string;
61
+ /** The whole search string, so `?alt=sse` is asserted as the literal it is. */
62
+ search?: string;
63
+ roles?: string[];
64
+ /** Every part's discriminating KEY (`text` / `functionCall` / `functionResponse` / `inlineData`), flattened in wire order. */
65
+ partKinds?: string[];
66
+ thinkingConfig?: unknown;
67
+ toolConfig?: unknown;
68
+ functionNames?: string[];
69
+ systemInstruction?: string;
70
+ maxOutputTokens?: number;
71
+ }
72
+ /** The discriminating key of one part, for an ordering assertion that does not depend on the payload. */
73
+ export declare function partKind(part: GeminiPart): string;
74
+ /**
75
+ * Asserts the EXACT request shape on the live request the fake received.
76
+ *
77
+ * `x-goog-api-key` is checked in its REDACTED form -- the base replaces a credential header's
78
+ * material as it records, so asserting on `***` proves both that the adapter authenticated and that
79
+ * the redaction ran.
80
+ */
81
+ export declare function assertGeminiRequest(recorded: RecordedRequest, expected?: GeminiRequestExpectation): void;
82
+ /**
83
+ * This dialect's equivalent ordering constraint, enforced so a pin about it can fail.
84
+ *
85
+ * A `functionResponse` answers the turn it follows; a text part placed ahead of one inside the same
86
+ * `user` entry is the shape this lane emitted for a decorated tool message. `inlineData` is exempt:
87
+ * an image the response's Struct could not carry rides beside it by construction.
88
+ *
89
+ * Returns the offending entry index, or `undefined` when every entry is well-formed.
90
+ */
91
+ export declare function findFunctionResponseOrderingViolation(recorded: RecordedRequest): number | undefined;
92
+ /**
93
+ * Role ALTERNATION, enforced so a pin about the merge can fail.
94
+ *
95
+ * `contents` is a conversation: two adjacent entries with the same role are not a turn the endpoint
96
+ * accepts. This lane's own serializer merges adjacent same-role messages precisely because of that —
97
+ * and then shipped a version where a message rendering to ZERO parts split the merge and produced
98
+ * two consecutive `user` entries, with nothing on the receiving end to notice. A fake that accepts
99
+ * any role sequence cannot fail a pin about role sequence.
100
+ *
101
+ * Returns the index of the second entry of the first offending pair, or `undefined` when the
102
+ * conversation alternates.
103
+ */
104
+ export declare function findRoleAlternationViolation(recorded: RecordedRequest): number | undefined;
105
+ export interface GeminiFakeOptions {
106
+ /** modelId -> the scripted answer for `:streamGenerateContent`. */
107
+ stream: Record<string, ((recorded: RecordedRequest, attempt: number) => Response | Promise<Response>) | Response[]>;
108
+ countTokens?: (recorded: RecordedRequest) => Response;
109
+ /** `GET /v1beta/models` -- discovery and credential validation both land here. */
110
+ models?: (recorded: RecordedRequest) => Response | Promise<Response>;
111
+ /** The path prefix the routes are mounted under. `/v1beta` for the Gemini API, `/v1/projects/...` for Vertex. */
112
+ prefix?: string;
113
+ }
114
+ /**
115
+ * The routes a Google-family adapter can reach.
116
+ *
117
+ * PREFIX-MATCHED (`path` ending in `*`) rather than exact, because the model id and the method are
118
+ * both IN THE PATH -- `/v1beta/models/gemini-2.5-pro:streamGenerateContent` -- so an exact match
119
+ * would need one route per scenario model.
120
+ */
121
+ export declare function geminiFakeRoutes(opts: GeminiFakeOptions): FakeRoute[];
@@ -0,0 +1,15 @@
1
+ export { errorResponse, jsonResponse, noRequestContains, redirectResponse, requestsTo, scenarioTable, sseResponse, stalledResponse, startFake, withFake, } from "./server.js";
2
+ export type { FakeRoute, FakeServer, RecordedRequest, ScenarioResponder, ScenarioTableOptions, SseFrame, SseResponseOptions, StartFakeOptions } from "./server.js";
3
+ export * as anthropicConsoleOauthFake from "./anthropic-console-oauth.js";
4
+ export * as anthropicFake from "./anthropic-messages.js";
5
+ export * as azureFake from "./azure-openai.js";
6
+ export * as bedrockFake from "./bedrock.js";
7
+ export * as codexFake from "./codex-oauth.js";
8
+ export * as geminiFake from "./gemini.js";
9
+ export * as openaiChatFake from "./openai-chat.js";
10
+ export * as openaiModelsFake from "./openai-models.js";
11
+ export * as openaiResponsesFake from "./openai-responses.js";
12
+ export * as vertexFake from "./vertex.js";
13
+ export * as xaiOauthFake from "./xai-oauth.js";
14
+ export { OPAQUE_FIELD_NAMES, redactOpaqueFields } from "./redact-opaque.js";
15
+ export { base64UrlDecodeBytes, base64UrlDecodeText, verifyRs256Jwt } from "./jwt-verify.js";
@@ -0,0 +1,56 @@
1
+ import {
2
+ startFake2,
3
+ withFake2,
4
+ sseResponse2,
5
+ jsonResponse2,
6
+ errorResponse2,
7
+ redirectResponse2,
8
+ stalledResponse2,
9
+ scenarioTable2,
10
+ requestsTo2,
11
+ noRequestContains2,
12
+ exports_openai_chat,
13
+ exports_openai_responses,
14
+ exports_azure_openai,
15
+ exports_anthropic_console_oauth,
16
+ OPAQUE_FIELD_NAMES2,
17
+ redactOpaqueFields2,
18
+ exports_anthropic_messages,
19
+ exports_bedrock,
20
+ exports_codex_oauth,
21
+ exports_gemini,
22
+ exports_openai_models,
23
+ base64UrlDecodeText2,
24
+ base64UrlDecodeBytes2,
25
+ verifyRs256Jwt2,
26
+ exports_vertex,
27
+ exports_xai_oauth
28
+ } from "../index-k4mhh1q5.js";
29
+ export {
30
+ OPAQUE_FIELD_NAMES2 as OPAQUE_FIELD_NAMES,
31
+ exports_anthropic_console_oauth as anthropicConsoleOauthFake,
32
+ exports_anthropic_messages as anthropicFake,
33
+ exports_azure_openai as azureFake,
34
+ base64UrlDecodeBytes2 as base64UrlDecodeBytes,
35
+ base64UrlDecodeText2 as base64UrlDecodeText,
36
+ exports_bedrock as bedrockFake,
37
+ exports_codex_oauth as codexFake,
38
+ errorResponse2 as errorResponse,
39
+ exports_gemini as geminiFake,
40
+ jsonResponse2 as jsonResponse,
41
+ noRequestContains2 as noRequestContains,
42
+ exports_openai_chat as openaiChatFake,
43
+ exports_openai_models as openaiModelsFake,
44
+ exports_openai_responses as openaiResponsesFake,
45
+ redactOpaqueFields2 as redactOpaqueFields,
46
+ redirectResponse2 as redirectResponse,
47
+ requestsTo2 as requestsTo,
48
+ scenarioTable2 as scenarioTable,
49
+ sseResponse2 as sseResponse,
50
+ stalledResponse2 as stalledResponse,
51
+ startFake2 as startFake,
52
+ verifyRs256Jwt2 as verifyRs256Jwt,
53
+ exports_vertex as vertexFake,
54
+ withFake2 as withFake,
55
+ exports_xai_oauth as xaiOauthFake
56
+ };
@@ -0,0 +1,13 @@
1
+ export declare function base64UrlDecodeText(value: string): string;
2
+ export declare function base64UrlDecodeBytes(value: string): Uint8Array;
3
+ /**
4
+ * Verifies a compact RS256 JWT against a public key and returns its claims.
5
+ *
6
+ * Exported for the LOOPBACK FAKE, which is the whole point: a fake that accepted any assertion would
7
+ * make "the adapter authenticated correctly" untestable, and the only way to check a signature is to
8
+ * check it. Nothing in the shipped path calls this.
9
+ */
10
+ export declare function verifyRs256Jwt(jwt: string, publicKey: CryptoKey): Promise<{
11
+ header: Record<string, unknown>;
12
+ claims: Record<string, unknown>;
13
+ } | undefined>;
@@ -0,0 +1,61 @@
1
+ import { type FakeRoute, type FakeServer, type RecordedRequest, type ScenarioResponder, type SseFrame } from "./server.js";
2
+ export interface ChatToolCallScript {
3
+ index: number;
4
+ id: string;
5
+ name: string;
6
+ /** Argument fragments, in order. The first fragment carries the id and name; later ones carry the index alone. */
7
+ argumentChunks: string[];
8
+ }
9
+ export interface ChatScript {
10
+ id?: string;
11
+ model?: string;
12
+ /** `delta.content` payloads, in order. */
13
+ text?: string[];
14
+ /** `delta.reasoning_content` payloads — DeepSeek's exposed reasoning channel. */
15
+ reasoning?: string[];
16
+ /** Emit the exposed channel under OpenRouter's `reasoning` spelling instead of DeepSeek's `reasoning_content`. */
17
+ reasoningFieldIsPlain?: boolean;
18
+ toolCalls?: ChatToolCallScript[];
19
+ finishReason?: "stop" | "tool_calls" | "length" | "content_filter";
20
+ usage?: {
21
+ prompt: number;
22
+ completion: number;
23
+ cachedPrompt?: number;
24
+ };
25
+ /** An error object inside a 200 stream. */
26
+ inlineError?: {
27
+ message: string;
28
+ code: string;
29
+ };
30
+ /** A tool-call fragment opening a NEW slot with no id/name — the no-silent-tool-dropping probe. */
31
+ anonymousToolCall?: boolean;
32
+ frameDelayMs?: number;
33
+ /** Omit the terminating `[DONE]` — the shape most local servers actually produce. */
34
+ omitDone?: boolean;
35
+ /** Omit the finish_reason too: a stream that simply stops. */
36
+ omitFinish?: boolean;
37
+ }
38
+ export declare function chatFrames(script: ChatScript): SseFrame[];
39
+ export declare function chatStream(script: ChatScript, opts?: {
40
+ dropAfter?: number;
41
+ }): Response;
42
+ export declare function chatModelOf(recorded: RecordedRequest): string | undefined;
43
+ /**
44
+ * The wire invariant OpenAI and Azure both enforce: a `tool` message must respond to the assistant
45
+ * `tool_calls` message immediately before it, with only other `tool` messages in between.
46
+ *
47
+ * MODELLED HERE ON PURPOSE. A fake that accepts anything cannot fail a pin, and round 3 is exactly
48
+ * that story: a decoration was rendered as a `user` message between an assistant's `tool_calls` and
49
+ * its `tool` reply, every fixture stayed green, and the shape would have failed every real turn.
50
+ * The DeepSeek and Azure fakes already model their providers' refusals; this closes the gap.
51
+ */
52
+ export declare function toolAdjacencyRefusal(body: string): Response | undefined;
53
+ export interface OpenAiChatFakeOptions {
54
+ scenarios: Record<string, ScenarioResponder | Response[]>;
55
+ routes?: FakeRoute[];
56
+ unknownModel?: ScenarioResponder;
57
+ }
58
+ /** Serves `/chat/completions` in every path spelling the family's surfaces use (bare, `/v1`, and Azure's deployment path). */
59
+ export declare function startOpenAiChatFake(opts: OpenAiChatFakeOptions): Promise<FakeServer>;
60
+ /** A DeepSeek-shaped 400 for a tool loop missing its prior `reasoning_content` — §6.3's hard error, reproduced. */
61
+ export declare function deepSeekMissingReasoningError(): Response;
@@ -0,0 +1,49 @@
1
+ import { type FakeRoute } from "./server.js";
2
+ export interface ModelRow {
3
+ id?: unknown;
4
+ display_name?: string;
5
+ context_window?: number;
6
+ [k: string]: unknown;
7
+ }
8
+ export interface ModelsPage {
9
+ rows: ModelRow[];
10
+ hasMore?: boolean;
11
+ }
12
+ export interface OpenAiModelsFakeOptions {
13
+ /**
14
+ * Pages, in order. Page N+1 is served when the request carries an `after` cursor equal to page
15
+ * N's last id — so a fixture proves the adapter actually PAGED rather than asking twice.
16
+ */
17
+ pages: ModelsPage[];
18
+ /**
19
+ * Answer the Nth call (1-based) AND EVERY CALL AFTER IT with this status.
20
+ *
21
+ * Persistent rather than a single blip, and the distinction is load-bearing: the cached-fallback
22
+ * probe needs a SECOND failure to prove that a failure with no cache PROPAGATES, and a one-shot
23
+ * failure quietly succeeds on that call instead.
24
+ */
25
+ failOnCall?: {
26
+ call: number;
27
+ status: number;
28
+ body?: unknown;
29
+ };
30
+ /** Serve a body that is not JSON. */
31
+ notJson?: boolean;
32
+ /** Path prefix, for a surface whose discovery does not sit at the root (Azure's `/openai/models`). */
33
+ pathPrefix?: string;
34
+ }
35
+ /**
36
+ * Routes for `/models` and `/v1/models`.
37
+ *
38
+ * The cursor is honoured rather than ignored: a page is chosen by matching `after` against the
39
+ * previous page's last id, so an adapter that sends no cursor (or the wrong one) re-reads page 1 and
40
+ * a duplicate-id assertion catches it.
41
+ */
42
+ export declare function openAiModelsRoutes(opts: OpenAiModelsFakeOptions): FakeRoute[];
43
+ /** Ollama's own `/api/tags` shape — the local adapter's second discovery door. */
44
+ export declare function ollamaTagsRoute(models: Array<{
45
+ name: string;
46
+ model?: string;
47
+ }>): FakeRoute;
48
+ /** A `/v1/models` that answers 404 — what a server which only speaks `/api/tags` does. */
49
+ export declare function modelsNotFoundRoutes(): FakeRoute[];
@@ -0,0 +1,86 @@
1
+ import { type FakeRoute, type FakeServer, type RecordedRequest, type ScenarioResponder, type SseFrame } from "./server.js";
2
+ /** A Responses turn, described by what it CONTAINS rather than by its frames. */
3
+ export interface ResponsesScript {
4
+ id?: string;
5
+ model?: string;
6
+ /** `response.output_text.delta` payloads, in order. */
7
+ text?: string[];
8
+ /** `response.reasoning_summary_text.delta` payloads — the readable summary channel. */
9
+ summary?: string[];
10
+ /** Completed reasoning items, by output index. `encrypted` becomes `encrypted_content`; an empty string means "summary only, nothing replayable". */
11
+ reasoningItems?: Array<{
12
+ index: number;
13
+ encrypted: string;
14
+ summaryText?: string;
15
+ }>;
16
+ calls?: Array<{
17
+ index: number;
18
+ itemId: string;
19
+ callId: string;
20
+ name: string;
21
+ /** When present, arguments arrive as `function_call_arguments.delta` frames. */
22
+ argumentChunks?: string[];
23
+ /** The complete `arguments` string on the final item. Defaults to the joined chunks. */
24
+ argumentsJson?: string;
25
+ }>;
26
+ usage?: {
27
+ input: number;
28
+ output: number;
29
+ cachedInput?: number;
30
+ };
31
+ /** Sets `incomplete_details.reason` on the completion event (`max_output_tokens` is the interesting one). */
32
+ incompleteReason?: string;
33
+ /** Adds a `refusal` content part to the completion's output message. */
34
+ refusal?: boolean;
35
+ /** An output item type this adapter cannot represent (`computer_call`, `mcp_call`, …) — the no-silent-tool-dropping probe. */
36
+ unrepresentableCall?: string;
37
+ /** Milliseconds before EVERY frame. The slow-stream / stall primitive. */
38
+ frameDelayMs?: number;
39
+ /** Emit `response.failed` instead of `response.completed`. */
40
+ failed?: string;
41
+ /** Omit the completion event entirely — a stream that just stops. */
42
+ omitCompleted?: boolean;
43
+ }
44
+ /**
45
+ * A script -> the SSE frames the real API produces, in the real order: created, summary, item
46
+ * openings, argument deltas, text, item completions, completion.
47
+ *
48
+ * The ORDER matters to more than one corpus case — `streaming-order` asserts it survives
49
+ * normalization unreordered, and `opaque-continuation` asserts the reasoning item is taken from the
50
+ * completion side of it rather than from `output_item.added`.
51
+ */
52
+ export declare function responsesFrames(script: ResponsesScript): SseFrame[];
53
+ /** A complete SSE response for one script. */
54
+ export declare function responsesStream(script: ResponsesScript, opts?: {
55
+ dropAfter?: number;
56
+ }): Response;
57
+ /** Reads the model id out of a Responses request body — the key `scenarioTable` dispatches on. */
58
+ export declare function responsesModelOf(recorded: RecordedRequest): string | undefined;
59
+ /** The parsed request body, for a serialization assertion. Always read off the FAKE'S RECORD, never off adapter intent. */
60
+ export declare function recordedBody(recorded: RecordedRequest): Record<string, unknown>;
61
+ /**
62
+ * The Responses twin of the chat surface's tool-message adjacency: a `function_call_output` must
63
+ * follow the `function_call` it answers, with only other outputs in between.
64
+ *
65
+ * Same reason as `toolAdjacencyRefusal` — a fake that accepts anything cannot fail a pin, and round
66
+ * 3's decoration bug inserted exactly such an item.
67
+ */
68
+ export declare function callPairingRefusal(body: string): Response | undefined;
69
+ export interface OpenAiResponsesFakeOptions {
70
+ /** modelId -> scripted answer. A `Response[]` is consumed by attempt, with the last entry repeating. */
71
+ scenarios: Record<string, ScenarioResponder | Response[]>;
72
+ /** Extra routes (a `/models` page set, a redirect target). */
73
+ routes?: FakeRoute[];
74
+ unknownModel?: ScenarioResponder;
75
+ }
76
+ /**
77
+ * Starts a fake serving `/responses` and `/v1/responses` (the adapter's base may or may not carry
78
+ * the `/v1` segment, and a fixture should not have to care which).
79
+ */
80
+ export declare function startOpenAiResponsesFake(opts: OpenAiResponsesFakeOptions): Promise<FakeServer>;
81
+ /** An OpenAI-shaped error body: `{ error: { message, type, code } }`, with `code` AFTER an unbounded human message (which is the whole reason `parseProviderErrorCode` reads the full body). */
82
+ export declare function openAiErrorBody(message: string, code: string, type?: string): Record<string, unknown>;
83
+ /** A non-JSON body, for the malformed-response case. */
84
+ export declare function htmlErrorResponse(status: number): Response;
85
+ /** A 200 whose body is not SSE at all — the other half of `error-malformed`. */
86
+ export declare function notSseResponse(): Response;
@@ -0,0 +1,4 @@
1
+ /** The field names whose VALUES are opaque provider state wherever they appear. */
2
+ export declare const OPAQUE_FIELD_NAMES: readonly string[];
3
+ /** A recorded body, safe to print. */
4
+ export declare function redactOpaqueFields(body: string): string;