@yanlinglabs/winter-provider-runtime 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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +41 -0
  3. package/README.md +109 -0
  4. package/dist/adapters/anthropic/console-oauth.d.ts +101 -0
  5. package/dist/adapters/anthropic/index.d.ts +4 -0
  6. package/dist/adapters/anthropic/messages.d.ts +76 -0
  7. package/dist/adapters/bedrock/converse.d.ts +143 -0
  8. package/dist/adapters/bedrock/crc32.d.ts +9 -0
  9. package/dist/adapters/bedrock/credentials.d.ts +32 -0
  10. package/dist/adapters/bedrock/eventstream.d.ts +65 -0
  11. package/dist/adapters/bedrock/index.d.ts +8 -0
  12. package/dist/adapters/bedrock/sigv4.d.ts +119 -0
  13. package/dist/adapters/bedrock/testing.d.ts +46 -0
  14. package/dist/adapters/content-blocks.d.ts +7 -0
  15. package/dist/adapters/google/adc.d.ts +35 -0
  16. package/dist/adapters/google/generate-content.d.ts +136 -0
  17. package/dist/adapters/google/index.d.ts +8 -0
  18. package/dist/adapters/google/jwt-rs256.d.ts +36 -0
  19. package/dist/adapters/google/vertex.d.ts +15 -0
  20. package/dist/adapters/index.d.ts +34 -0
  21. package/dist/adapters/oauth/device-code.d.ts +32 -0
  22. package/dist/adapters/oauth/refresh.d.ts +40 -0
  23. package/dist/adapters/openai/azure.d.ts +38 -0
  24. package/dist/adapters/openai/chat-completions.d.ts +86 -0
  25. package/dist/adapters/openai/codex-config.d.ts +42 -0
  26. package/dist/adapters/openai/codex-oauth.d.ts +47 -0
  27. package/dist/adapters/openai/index.d.ts +20 -0
  28. package/dist/adapters/openai/local.d.ts +16 -0
  29. package/dist/adapters/openai/pkce.d.ts +111 -0
  30. package/dist/adapters/openai/quota.d.ts +99 -0
  31. package/dist/adapters/openai/responses.d.ts +142 -0
  32. package/dist/adapters/openai/shared.d.ts +359 -0
  33. package/dist/adapters/openai/testing.d.ts +59 -0
  34. package/dist/adapters/openai/xai-derived-shapes.d.ts +67 -0
  35. package/dist/adapters/openai/xai-oauth.d.ts +102 -0
  36. package/dist/adapters/openai/xai-oauth.testing.d.ts +62 -0
  37. package/dist/adapters/privileged-headers.d.ts +51 -0
  38. package/dist/adapters/refusals.d.ts +10 -0
  39. package/dist/address-classifier.d.ts +17 -0
  40. package/dist/bun-required.d.ts +54 -0
  41. package/dist/continuity/decoration.d.ts +89 -0
  42. package/dist/continuity/domains.d.ts +92 -0
  43. package/dist/continuity/fixtures.d.ts +44 -0
  44. package/dist/continuity/handoff.d.ts +94 -0
  45. package/dist/continuity/index.d.ts +10 -0
  46. package/dist/continuity/renderer.d.ts +111 -0
  47. package/dist/continuity/warnings.d.ts +46 -0
  48. package/dist/credentials/env.d.ts +6 -0
  49. package/dist/credentials/file.d.ts +21 -0
  50. package/dist/credentials/memory.d.ts +8 -0
  51. package/dist/credentials/types.d.ts +38 -0
  52. package/dist/discovery.d.ts +15 -0
  53. package/dist/endpoint-policy.d.ts +127 -0
  54. package/dist/errors.d.ts +68 -0
  55. package/dist/http.d.ts +29 -0
  56. package/dist/identity.d.ts +62 -0
  57. package/dist/index-5z94gxhk.js +43790 -0
  58. package/dist/index.d.ts +39 -0
  59. package/dist/index.js +3194 -0
  60. package/dist/registry.d.ts +136 -0
  61. package/dist/retry.d.ts +38 -0
  62. package/dist/sse.d.ts +12 -0
  63. package/dist/testing.d.ts +19 -0
  64. package/dist/testing.js +432 -0
  65. package/dist/types.d.ts +376 -0
  66. package/package.json +49 -0
@@ -0,0 +1,359 @@
1
+ import type { WinterModelDescriptor } from "@yanlinglabs/winter-provider-catalog";
2
+ import { type IdentityHeaderLookup } from "../../identity.js";
3
+ import { type EndpointPolicy } from "../../endpoint-policy.js";
4
+ import { ProviderRequestError } from "../../http.js";
5
+ import { type RetryPolicy, type RetryPolicyOptions } from "../../retry.js";
6
+ import type { ContentBlockLike, CredentialMaterial, CredentialRef, CredentialStatus, DiscoveryContext, ModelCatalogResult, ProviderContext, ProviderEvent, ProviderMessageLike, TurnRequest } from "../../types.js";
7
+ /** How this surface carries a key: OpenAI-style `Authorization: Bearer`, or Azure's own `api-key` header. */
8
+ export type AuthStyle = "bearer" | "azure-api-key";
9
+ /** Looks a provider-local model id up in the catalog. See decision 2 in this file's header. */
10
+ export type DescriptorLookup = (providerLocalModelId: string) => WinterModelDescriptor | undefined;
11
+ export interface OpenAiAdapterOptions {
12
+ /**
13
+ * The adapter's OWN reviewed endpoint — the one `applyPrivilegedHeaders` will speak to. Defaults
14
+ * to the family's vendor URL; overridable at CONSTRUCTION only, which is what lets a fixture point
15
+ * a generated endpoint at a loopback fake without turning `ConnectionProfile.baseUrl` (untrusted)
16
+ * into a privileged one.
17
+ */
18
+ generatedBaseUrl?: string;
19
+ /**
20
+ * REQUIRED — omitting it is a compile error, and that is the fail-closed mechanism (ruling on
21
+ * finding I3).
22
+ *
23
+ * Nothing in this file's logic changes when a lookup is absent; what changes is that EVERY WS-13
24
+ * §8.2 refusal quietly stops happening: an unmapped effort passes through, an over-limit request
25
+ * is sent, a thinking config on a model with no reasoning evidence is honoured, and — worst,
26
+ * because it is silent and remote — a DeepSeek profile stops capturing `reasoning_content` and
27
+ * 400s on the second leg of every tool loop. None of that fails to compile, and none of it fails
28
+ * a test that did not think to look. Requiring the field is the only guard that cannot be
29
+ * forgotten.
30
+ *
31
+ * The gateway/unlisted shape is an EXPLICIT `() => undefined`: a caller saying "this model has no
32
+ * catalog evidence" out loud, rather than a caller who forgot.
33
+ */
34
+ descriptors: DescriptorLookup;
35
+ /** Injected in tests so a retry fixture never sleeps a real backoff. */
36
+ retry?: RetryPolicyOptions;
37
+ /** R6-L PRIVILEGED: an organisation identifier only means something at the reviewed endpoint it was minted for. */
38
+ organization?: string;
39
+ /** R6-L PRIVILEGED, same reasoning as `organization`. */
40
+ project?: string;
41
+ /** The cap on a STREAM's total bytes. Generous by design: a long generation is legitimately large. */
42
+ maxBodyBytes?: number;
43
+ /** Milliseconds allowed for response HEADERS. Cleared once they arrive — never a bound on the generation. */
44
+ headerTimeoutMs?: number;
45
+ /** How this surface carries a key. Azure's deployment path wants `api-key`; every other surface is a bearer. */
46
+ authStyle?: AuthStyle;
47
+ /**
48
+ * WS-13b §7/§8.4 (fix-wave R-FW-2): the per-ROW second identity field, looked up by
49
+ * `ctx.connection.providerId`.
50
+ *
51
+ * A LOOKUP rather than a value, because this family's adapters are multi-provider: one
52
+ * `winter.openai-chat-completions` instance serves a hundred and thirty-six rows, so "the identity
53
+ * headers" is a property of the row a turn is for, not of the adapter. Optional, and absent means
54
+ * `{}` — only a vendor that NAMES a second identity field gets one.
55
+ */
56
+ identityHeaders?: IdentityHeaderLookup;
57
+ }
58
+ export declare const DEFAULT_STREAM_BODY_BYTES: number;
59
+ export declare const DEFAULT_HEADER_TIMEOUT_MS = 60000;
60
+ /** A `/v1/models` page count that is a loop rather than a catalog. Discovery is additionally item- and time-bounded by `discoverModels`. */
61
+ export declare const MAX_DISCOVERY_PAGES = 20;
62
+ /** A refusal raised BEFORE any request is sent (WS-13 §8.2). `capability` is the Winter code for "this selection cannot be represented on this wire". */
63
+ export declare function capabilityRefusal(reason: string): ProviderRequestError;
64
+ /** A refusal about the request's own shape, raised before it is sent. */
65
+ export declare function badRequestRefusal(reason: string): ProviderRequestError;
66
+ /**
67
+ * This family's twin of `THINKING_ENABLED_NEEDS_BUDGET` (`adapters/refusals.ts`): the same caller
68
+ * mistake — "reasoning on, but at what?" — asked in this family's own vocabulary, because the way
69
+ * out is an EFFORT here and a budget there. Same sentence shape, so a caller moving a session
70
+ * between families recognises the situation without re-reading it.
71
+ */
72
+ export declare function THINKING_ENABLED_NEEDS_EFFORT(model: string): string;
73
+ export interface ResolvedEndpoint {
74
+ /** The origin+path prefix every request URL is built from. No trailing slash. */
75
+ baseUrl: string;
76
+ policy: EndpointPolicy;
77
+ /** True when the base came from the ADAPTER (reviewed) rather than from `ConnectionProfile.baseUrl`. */
78
+ generated: boolean;
79
+ }
80
+ /**
81
+ * Chooses between the adapter's generated endpoint and the profile's base URL, and builds the
82
+ * policy `boundedFetch` enforces.
83
+ *
84
+ * A profile `baseUrl` is evaluated by its ORIGIN, not by its mere presence (P7a): a reviewed
85
+ * endpoint the runtime COPIED in (`endpointOrigin: "reviewed"` — every row on an adapter that
86
+ * serves several providers) stays generated, and only a host- or user-supplied one is a USER
87
+ * endpoint. `connectionEndpointOptions` is the single reading of that field, and it carries the
88
+ * profile's own `local` declaration through — which is what lets a loopback Ollama be reached over
89
+ * plain http while an undeclared private address is still refused (`evaluateEndpoint`'s own rule,
90
+ * not a second copy of it here).
91
+ */
92
+ export declare function resolveEndpoint(ctx: ProviderContext, options: OpenAiAdapterOptions, fallbackGeneratedBaseUrl?: string): ResolvedEndpoint;
93
+ export interface ResolvedAuth {
94
+ headers: Record<string, string>;
95
+ material: CredentialMaterial | null;
96
+ }
97
+ /**
98
+ * Resolves `ctx.authRef` into request headers.
99
+ *
100
+ * A `null` material (a `none` ref, or a keychain slot with nothing in it) is NOT an error here: a
101
+ * local server with `authKind: "local-none"` is a first-class configuration, and the surfaces that
102
+ * genuinely require a credential say so themselves by refusing an empty header set.
103
+ */
104
+ export declare function resolveAuth(ctx: ProviderContext, style: AuthStyle): Promise<ResolvedAuth>;
105
+ export interface HeaderPlan {
106
+ policy: EndpointPolicy;
107
+ /** Everything an endpoint needs to be spoken to at all: content-type, accept, `OpenAI-Beta`, and auth. NOT routed through `applyPrivilegedHeaders`. */
108
+ protocol: Record<string, string>;
109
+ /** Identifiers that only mean something at the reviewed endpoint they were minted for (R6-L). */
110
+ privileged?: Record<string, string>;
111
+ /**
112
+ * WINTER'S OWN second identity field for this row (`Client-Agent`), already `<version>`-substituted.
113
+ *
114
+ * Spread with the `user-agent`, BEFORE the host's map and NOT through `applyPrivilegedHeaders` —
115
+ * it is Winter's name, not the operator's account topology, and gating it on a generated endpoint
116
+ * would drop it for every multi-provider row (whose reviewed endpoint is copied into the profile
117
+ * and evaluated as a user endpoint). `identityFor` builds it.
118
+ */
119
+ identity?: Record<string, string>;
120
+ /** `ConnectionProfile.headers` — the host's own additions (OpenRouter's attribution pair, a proxy token's sibling header). */
121
+ userSupplied?: Record<string, string> | undefined;
122
+ }
123
+ /**
124
+ * Assembles a request's headers.
125
+ *
126
+ * ORDER IS THE POINT: user-supplied first, privileged next, protocol last, so an adapter-owned
127
+ * header can never be displaced by a profile. And a user-supplied set is stripped of every
128
+ * credential-bearing NAME first — a `ConnectionProfile` is non-secret connection metadata by
129
+ * contract (WS-13 §6), so a credential appearing there is a misconfiguration to drop, not a second
130
+ * auth channel to honour.
131
+ */
132
+ export declare function buildHeaders(plan: HeaderPlan): Record<string, string>;
133
+ /** This row's Winter-authored identity headers, for a `HeaderPlan`. One call shape for every site in the family. */
134
+ export declare function identityFor(options: Pick<OpenAiAdapterOptions, "identityHeaders">, ctx: ProviderContext): Record<string, string>;
135
+ /** The pinned five-tier ladder (`sdk.d.ts:586`), in order. The rank is what a NUMERIC effort is snapped onto. */
136
+ export declare const EFFORT_LADDER: readonly ["low", "medium", "high", "xhigh", "max"];
137
+ export type LadderEffort = (typeof EFFORT_LADDER)[number];
138
+ /**
139
+ * A numeric effort -> the nearest tier the MODEL verifies.
140
+ *
141
+ * The pin states no unit, no range and no mapping for the numeric form (derived-shapes-p6.md item
142
+ * (c): "the answer is a documented absence", OQ-P6-2), so this is gap-filling and is disclosed. The
143
+ * rule chosen, stated so it is auditable: the integer is a POSITION ON THE PINNED FIVE-TIER LADDER,
144
+ * clamped to [1, 5] — so `3` means "high" on every model — and is then snapped to the nearest tier
145
+ * this model's own `reasoning.efforts` actually lists. That keeps the number's meaning stable across
146
+ * models (an index-into-the-model's-list reading would make `2` mean different things on a 3-tier
147
+ * and a 5-tier model) while still never sending a value the model has not verified.
148
+ */
149
+ export declare function snapNumericEffort(value: number, verified: readonly string[]): string | undefined;
150
+ /**
151
+ * WS-13 §8.2's rule as a function: map onto the model's VERIFIED vocabulary, or reject BEFORE the
152
+ * request. A silent downgrade to the provider's default is prohibited, which is why every failure
153
+ * arm returns `{ ok: false }` rather than `undefined`.
154
+ *
155
+ * The descriptor-less arm (a gateway model passed through by `allowUnlisted`) is deliberately
156
+ * different rather than lax: a NAMED effort is passed through verbatim (the caller named a tier the
157
+ * pin defines and Winter has no evidence to contradict it), while a NUMERIC one is refused, because
158
+ * snapping a number needs a vocabulary and there is none.
159
+ */
160
+ export declare function mapEffortAgainst(effort: TurnRequest["effort"], descriptor: WinterModelDescriptor | undefined): {
161
+ ok: true;
162
+ value: string | undefined;
163
+ } | {
164
+ ok: false;
165
+ reason: string;
166
+ };
167
+ export interface ReasoningPlan {
168
+ /** The `reasoning.effort` value, when one applies. */
169
+ effort?: string;
170
+ /** The `reasoning.summary` value, when the descriptor's `summaryRequest` evidence says how to ask. */
171
+ summary?: string;
172
+ /** True when the request should carry `include: ["reasoning.encrypted_content"]` (Responses only). */
173
+ wantsEncryptedContent: boolean;
174
+ /** False when `thinking: {type:"disabled"}` — no reasoning object at all. */
175
+ enabled: boolean;
176
+ }
177
+ /**
178
+ * Resolves `effort` + `thinking` into what the wire will carry, or throws a typed refusal.
179
+ *
180
+ * THE FAMILY'S RULE IN ONE LINE (WS-13 §8.2, fix-wave ruling F-2): this family has NO budget field,
181
+ * so `thinking: { type: "enabled" }` means reasoning ON at the row's own `defaultEffort` — the only
182
+ * effort the catalog verified for that model, never an invented one.
183
+ *
184
+ * The three refusals worth stating, because each has a tempting silent alternative:
185
+ *
186
+ * `thinking: {type:"enabled", budgetTokens: N}` is REFUSED. The OpenAI surfaces have no
187
+ * token-budget knob for reasoning — the verified vocabulary is effort tiers — so honouring the
188
+ * config would mean dropping the budget and sending an effort the caller never asked for. That
189
+ * is precisely the silent downgrade WS-13 §8.2 prohibits.
190
+ *
191
+ * `thinking: {type:"enabled"|"adaptive"}` on a model with NO reasoning evidence is REFUSED rather
192
+ * than ignored, for the same reason: "we quietly did not think" is not an outcome a caller can
193
+ * see.
194
+ *
195
+ * `thinking: {type:"enabled"|"adaptive"}` with NO effort given and NO `defaultEffort` on the row
196
+ * is REFUSED — and this one was the whole-branch review's M-9. It used to resolve to `effort:
197
+ * undefined`, which `buildResponsesBody` renders as `include: ["reasoning.encrypted_content"]`
198
+ * with NO `reasoning` object at all: a request that asks to keep reasoning state for reasoning
199
+ * it never asked the model to do. The turn succeeds, the caller is told nothing, and the answer
200
+ * is the no-think one. Same class as the two above, so it gets the same treatment.
201
+ */
202
+ export declare function resolveReasoning(req: TurnRequest, descriptor: WinterModelDescriptor | undefined): ReasoningPlan;
203
+ /**
204
+ * WS-13 §8.2's other half: a request over the model's DECLARED limits, or naming a parameter the
205
+ * model rejects, fails here — before a byte goes out — rather than upstream.
206
+ */
207
+ export declare function assertWithinLimits(req: TurnRequest, descriptor: WinterModelDescriptor | undefined, parametersInPlay: readonly string[]): void;
208
+ /** The three-state tool capability plus the reasoning facts the bridge reads off an adapter. */
209
+ export declare function capabilitiesFrom(descriptor: WinterModelDescriptor): {
210
+ toolCalling: "native" | "emulated" | "none";
211
+ continuationDomain?: string;
212
+ readableState: "none" | "summary" | "full-exposed";
213
+ };
214
+ /**
215
+ * A tool the adapter cannot represent is an ERROR, never a silently dropped tool (WS-13 §9's own
216
+ * hard negative). "Cannot represent" is narrow and structural: a tool with no name, or a schema that
217
+ * is not a JSON-Schema object, is one the provider would reject or — worse — accept while quietly
218
+ * losing the constraint.
219
+ */
220
+ export declare function assertRepresentableTools(tools: TurnRequest["tools"]): void;
221
+ /** Normalizes `string | ContentBlockLike[]` to blocks, so a mapper has one shape to walk. */
222
+ export declare function asBlocks(content: ProviderMessageLike["content"]): ContentBlockLike[];
223
+ /** Flattens a `tool_result.content` (which R6-3 widened to `string | ContentBlockLike[]`) into the plain text every OpenAI surface carries. */
224
+ export declare function toolResultText(content: string | ContentBlockLike[]): string;
225
+ /**
226
+ * A Winter-authored annotation -> the text that actually rides the wire (minor 11).
227
+ *
228
+ * BOTH DOORS RENDER AS PLAIN TEXT on this family, and that is a decision rather than a shortcut.
229
+ * `thinking-channel` names an in-dialect reasoning slot; no OpenAI-family surface has one a caller
230
+ * may write into, and the nearest thing (`reasoning_content`) is the MODEL's own output channel —
231
+ * putting Winter's prose there would present an annotation as something the model reasoned, which is
232
+ * the impersonation R6-8 exists to forbid. So the annotation is carried plainly, on both doors.
233
+ *
234
+ * VERBATIM — this layer adds NOTHING, not even a label. The text arrives from Lane C already
235
+ * finished and already delimited (the `<recovered_reasoning_summary>` tag WS-13 §8.2 names for the
236
+ * tag door, the bracketed label for the thinking-channel door), and Lane C's §9.6 budget is counted
237
+ * on exactly these bytes. A wrapper of this layer's own would double-label the second door, would
238
+ * add a delimiter `neutralizeDelimiters` does not neutralise (so a foreign summary containing the
239
+ * added closing delimiter would break straight out of it), and would make this family the only one
240
+ * that alters the string — the whole-branch review's I-3, escalated from Lane B's identical
241
+ * `<winter-note>` wrapper. The other three families render it byte-for-byte; so does this one.
242
+ *
243
+ * Without this door at all, Lane C's decorations were built, persisted and then silently dropped at
244
+ * the wire: a cross-family handoff note that never reaches the model is worse than none, because the
245
+ * switch's `continuity_warning` has already reported the context as carried.
246
+ *
247
+ * WHERE it goes depends on what the message carries. On an ordinary message it LEADS the content.
248
+ * On a message carrying TOOL RESULTS it PREFIXES the first result's own text (see
249
+ * `prefixToolResult`) — never a message of its own, because a message between an assistant's
250
+ * `tool_calls` and its `tool` reply is rejected outright ("messages with role 'tool' must be a
251
+ * response to a preceeding message with 'tool_calls'"). Round 3's finding: an annotation that
252
+ * breaks the turn is worse than one that is dropped.
253
+ */
254
+ export declare function decorationText(message: ProviderMessageLike): string | undefined;
255
+ /**
256
+ * A decoration prefixed onto a tool result's own text.
257
+ *
258
+ * Adjacency between a tool call and its result is a WIRE INVARIANT on every surface in this family,
259
+ * so the annotation rides INSIDE the result it annotates rather than beside it. Same verbatim text,
260
+ * same position relative to what it describes, and no extra item on the wire at all.
261
+ */
262
+ export declare function prefixToolResult(decoration: string | undefined, output: string): string;
263
+ /** An `image` block -> the data URL every OpenAI surface accepts. */
264
+ export declare function imageDataUrl(block: Extract<ContentBlockLike, {
265
+ type: "image";
266
+ }>): string;
267
+ /**
268
+ * A one-slot async queue for events produced by a SYNCHRONOUS callback while the generator that
269
+ * must yield them is suspended. See decision 3 in this file's header.
270
+ */
271
+ export declare class EventQueue {
272
+ private items;
273
+ private waker;
274
+ push(event: ProviderEvent): void;
275
+ wake(): void;
276
+ drain(): ProviderEvent[];
277
+ wait(): Promise<void>;
278
+ }
279
+ /**
280
+ * Runs `work` while yielding whatever lands on `queue`, and returns `work`'s value.
281
+ *
282
+ * The ordering this buys is the whole point: `withRetry` announces a retry BEFORE it sleeps, so the
283
+ * event is queued, the pump wakes on the next microtask, and the consumer sees it before the retried
284
+ * request reaches the wire. A post-hoc flush would report every retry after the attempt that
285
+ * finally succeeded.
286
+ */
287
+ export declare function pumpEvents<T>(queue: EventQueue, work: Promise<T>): AsyncGenerator<ProviderEvent, T>;
288
+ export interface StreamingRequestPlan {
289
+ url: string;
290
+ headers: Record<string, string>;
291
+ body: string;
292
+ policy: EndpointPolicy;
293
+ ctx: ProviderContext;
294
+ options: OpenAiAdapterOptions;
295
+ signal?: AbortSignal | undefined;
296
+ /** Called once per attempt, BEFORE the request goes out. The codex quota manager's hook. */
297
+ beforeAttempt?: (attempt: number) => Promise<void>;
298
+ /** Given a non-2xx response, decides whether the adapter can recover in-band (codex's one-shot token refresh). Returning a new header set retries immediately, outside the retry budget. */
299
+ recover?: (status: number, attempt: number) => Promise<Record<string, string> | undefined>;
300
+ /**
301
+ * Observes the REFUSED response before its body is read, so an adapter can take a fact off the
302
+ * headers that the normalized error does not carry onward.
303
+ *
304
+ * The one caller is codex's quota manager, and the reason it needs this door rather than the
305
+ * `retry` event is finding I1: `retry.retryDelayMs` is `Retry-After` only when the backend sent
306
+ * one, and is Winter's own jittered backoff otherwise — so reading the window off the event
307
+ * fabricated a subscription reset time out of local jitter. This hands over the header itself,
308
+ * present or absent.
309
+ */
310
+ onRefused?: (response: Response) => void;
311
+ }
312
+ /**
313
+ * Opens a streaming POST under the retry policy, and returns the response whose body the caller will
314
+ * consume.
315
+ *
316
+ * `policy.commit()` is called by the CALLER, the moment it consumes the first byte — not here.
317
+ * Committing at header time would forbid retrying a 503 that arrived with headers and no body, and
318
+ * committing never would allow replaying a turn whose tool call the caller already executed. The
319
+ * first byte is the line WS-13 §13 actually draws.
320
+ */
321
+ export declare function openStream(plan: StreamingRequestPlan, policy: RetryPolicy, onEvent: (event: ProviderEvent) => void): Promise<Response>;
322
+ /** Reads an error response's body (bounded by `boundedFetch` already) and normalizes it. Never logs the body. */
323
+ export declare function httpErrorFrom(response: Response): Promise<ProviderRequestError>;
324
+ /** Turns anything thrown during a turn into the `error` event the fold converts to a `ProviderTurnError`. */
325
+ export declare function errorEvent(err: unknown): Extract<ProviderEvent, {
326
+ type: "error";
327
+ }>;
328
+ export declare function makeRetryPolicy(options: OpenAiAdapterOptions): RetryPolicy;
329
+ /**
330
+ * `GET {base}/models`, paged.
331
+ *
332
+ * BOUNDED THREE WAYS and none of them is advisory: `boundedFetch` caps the bytes of each page at
333
+ * `ctx.limits.maxBytes`, `MAX_DISCOVERY_PAGES` caps the hops, and `ctx.limits.maxItems` caps the
334
+ * rows. `partial: true` whenever a bound stopped the walk — the caller must never read a truncated
335
+ * list as "these are all the models that exist" (which, for an authoritative provider, would read
336
+ * the absent ones as removed).
337
+ *
338
+ * Sanitisation, dedup and malformed-row rejection are `discoverModels`'s, deliberately: this
339
+ * function's job is to fetch honestly, and having two places decide what a valid model id is, is how
340
+ * they drift.
341
+ */
342
+ export declare function fetchOpenAiModels(ctx: DiscoveryContext, endpoint: ResolvedEndpoint, headers: Record<string, string>, options: OpenAiAdapterOptions,
343
+ /** Query parameters every discovery page must carry (Azure's mandatory `api-version`). */
344
+ extraQuery?: Record<string, string>): Promise<ModelCatalogResult>;
345
+ /** One `/v1/models` row -> the catalog-result shape. Untrusted: nothing here is interpolated anywhere, and `discoverModels` does the validation. */
346
+ export declare function rowToModel(row: unknown): ModelCatalogResult["models"][number];
347
+ /**
348
+ * The family's shared `validateCredential`: a bounded `GET /models` with the credential attached.
349
+ *
350
+ * `unsupported` means the adapter cannot CHECK this ref kind — never that the credential is bad. A
351
+ * `none` ref against a non-local endpoint is `missing`, which is the actionable answer.
352
+ */
353
+ export declare function validateViaModels(ref: CredentialRef, ctx: ProviderContext, endpoint: ResolvedEndpoint, headers: Record<string, string>, options: OpenAiAdapterOptions, hasCredential: boolean,
354
+ /** Query parameters the probe must carry. Azure rejects EVERY call without `api-version`, so omitting it made a valid key report as unreachable. */
355
+ extraQuery?: Record<string, string>): Promise<CredentialStatus>;
356
+ /** `[DONE]` is an OpenAI-family convention rather than an SSE one, so `parseSse` passes it through and the adapter recognises it here. */
357
+ export declare function isStreamTerminator(data: string): boolean;
358
+ /** Parses one SSE `data:` payload. A junk frame is tolerated (returns `undefined`) — forward compatibility, and providers do emit them. */
359
+ export declare function parseSseJson(data: string): Record<string, unknown> | undefined;
@@ -0,0 +1,59 @@
1
+ import type { CapabilityEvidence, WinterModelDescriptor } from "@yanlinglabs/winter-provider-catalog";
2
+ import type { CredentialRef, DiscoveryContext, ProviderContext, ProviderEvent } from "../../types.js";
3
+ export declare const TEST_API_KEY = "test-key-openai-0000";
4
+ export declare function evidence<T>(value: T, confidence?: CapabilityEvidence<T>["confidence"]): CapabilityEvidence<T>;
5
+ export interface DescriptorOverrides {
6
+ key?: string;
7
+ providerId?: string;
8
+ upstreamId?: string;
9
+ efforts?: string[];
10
+ defaultEffort?: string;
11
+ readableState?: "none" | "summary" | "full-exposed";
12
+ summaryValues?: string[];
13
+ maxOutputTokens?: number;
14
+ unsupportedParameters?: string[];
15
+ toolCalling?: "native" | "emulated" | "none";
16
+ continuationDomain?: string[];
17
+ continuation?: "none" | "plaintext" | "opaque-provider-state" | "server-response-handle";
18
+ noReasoning?: boolean;
19
+ inputModalities?: string[];
20
+ parallelTools?: boolean;
21
+ }
22
+ /** A descriptor shaped like the seed catalog's rows, with only the fields a fixture cares about varied. */
23
+ export declare function descriptor(overrides?: DescriptorOverrides): WinterModelDescriptor;
24
+ export interface TestContextOptions {
25
+ providerId?: string;
26
+ baseUrl?: string;
27
+ local?: boolean;
28
+ headers?: Record<string, string>;
29
+ deployment?: string;
30
+ apiVersion?: string;
31
+ authRef?: CredentialRef;
32
+ apiKey?: string | null;
33
+ stallTimeoutMs?: number;
34
+ logs?: Array<{
35
+ kind: string;
36
+ providerId: string;
37
+ model?: string;
38
+ bytes?: number;
39
+ }>;
40
+ }
41
+ export declare function testContext(opts?: TestContextOptions): ProviderContext;
42
+ export declare function testDiscoveryContext(opts?: TestContextOptions & {
43
+ maxItems?: number;
44
+ maxBytes?: number;
45
+ timeoutMs?: number;
46
+ signal?: AbortSignal;
47
+ }): DiscoveryContext;
48
+ /** A retry policy that never actually waits — one tick is enough for the observation pump to run, and a real backoff would eat a test budget. */
49
+ export declare const FAST_RETRY: {
50
+ maxRetries: number;
51
+ random: () => number;
52
+ sleep: (_ms: number) => Promise<void>;
53
+ };
54
+ /** Drains an adapter stream. The events, in order, exactly as a consumer would see them. */
55
+ export declare function collect(stream: AsyncIterable<ProviderEvent>): Promise<ProviderEvent[]>;
56
+ /** The single `error` event a refused turn produces, or a failure naming what came instead. */
57
+ export declare function soleError(events: ProviderEvent[]): Extract<ProviderEvent, {
58
+ type: "error";
59
+ }>;
@@ -0,0 +1,67 @@
1
+ /** `github.com/xai-org/grok-build`, Apache-2.0, at the commit the capture pinned. Recorded in the repository-root `NOTICE`. */
2
+ export declare const DERIVED_XAI_COMMIT = "72a61251fcffb464bcc687aeb5a998e5a98ec0c9";
3
+ /**
4
+ * The xAI OAuth constants as the vendor's own public client states them.
5
+ *
6
+ * Cross-checked, where possible, against a SECOND independent artifact: the authorization server's
7
+ * own discovery document at `https://auth.x.ai/.well-known/openid-configuration`, which names the
8
+ * same device-authorization and token endpoints and lists `none` among its supported token-endpoint
9
+ * auth methods — i.e. confirms the client is public and secret-less.
10
+ */
11
+ export declare const DERIVED_XAI: {
12
+ /** capture §2 — `auth/config.rs:250`. Published in Apache-2.0 source; `obfstr!`-wrapped in the shipped binary, which is binary hardening, not a secret. */
13
+ readonly clientId: "b1a00492-073a-47ea-816f-4c329264a828";
14
+ /** capture §2 — `auth/config.rs:122`. */
15
+ readonly issuer: "https://auth.x.ai";
16
+ /** capture §2 — `auth/config.rs:14-27`, frozen by the vendor client's own contract test at `config.rs:426-446`. All ten, unchanged: see capture §5.3 for why Winter does not narrow them. */
17
+ readonly scope: "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write workspaces:read workspaces:write";
18
+ /** capture §2/§3.1 — `auth/config.rs:118-120,150` and `auth/device_code.rs:145`. The form field the flow carries a client identity in. */
19
+ readonly identityField: "referrer";
20
+ /** capture §2 — the value the VENDOR's client sends in that field. Winter sends its own name instead (capture §5.2); this is here so a reader can see the difference the codex shape turns on. */
21
+ readonly vendorIdentityValue: "grok-build";
22
+ /** capture §2/§3.1 — `auth/device_code.rs:132`, confirmed by the discovery document's `device_authorization_endpoint`. */
23
+ readonly deviceCodeUrl: "https://auth.x.ai/oauth2/device/code";
24
+ /** capture §2/§3.2 — `auth/device_code.rs:199`, confirmed by the discovery document's `token_endpoint`. */
25
+ readonly tokenUrl: "https://auth.x.ai/oauth2/token";
26
+ /**
27
+ * capture §4 — where an OAuth/subscription bearer is actually spent.
28
+ *
29
+ * NOT `https://api.x.ai/v1`, which the task brief and WS-13b §2 both name: that is the METERED,
30
+ * api-key surface (lane X2's separate `xai` row). This one is the proxy the vendor's own
31
+ * installers default to and the host whose 401s report `auth_kind=bearer`. Pointing a
32
+ * subscription token at the metered endpoint either fails to authenticate or bills a user for
33
+ * traffic their subscription already covers.
34
+ */
35
+ readonly apiBaseUrl: "https://cli-chat-proxy.grok.com/v1";
36
+ /**
37
+ * capture §3.4 — the vendor client's OWN identity and telemetry headers, ALL SIX.
38
+ *
39
+ * Recorded so they can be asserted ABSENT, on the login path and the generation path alike.
40
+ * WS-13 §5: client-identity headers are never imported; Winter adapters author their own. This
41
+ * list is a DENYLIST for Winter's requests, never a template for them.
42
+ *
43
+ * The last two are the ones that matter most, and they are not telemetry: the vendor's client
44
+ * injects `X-XAI-Token-Auth: xai-grok-cli` and `x-authenticateresponse` **only when the base URL
45
+ * is the cli-chat-proxy** — i.e. exactly the endpoint this row uses — and `xai-grok-cli` is a
46
+ * first-party product identity Winter may not send. If the proxy REQUIRES it, this row's
47
+ * inference path is closed to an honest client. See capture §3.4 and the inference-path reversion
48
+ * condition in §7.
49
+ */
50
+ readonly vendorOnlyHeaders: readonly ["x-grok-client-version", "x-grok-client-surface", "x-grok-client-identifier", "x-grok-client-mode", "x-xai-token-auth", "x-authenticateresponse"];
51
+ };
52
+ /**
53
+ * The models the vendor's subscription client offers, from its own catalogue
54
+ * (`xai-grok-models/default_models.json`) — capture §6.
55
+ *
56
+ * Deliberately unpriced: this row is subscription-priced, and a per-token price on it would
57
+ * misreport cost for traffic that is not metered.
58
+ */
59
+ export declare const DERIVED_XAI_MODELS: readonly [{
60
+ readonly id: "grok-4.6";
61
+ readonly contextWindow: 500000;
62
+ readonly isDefault: true;
63
+ }, {
64
+ readonly id: "grok-4.5";
65
+ readonly contextWindow: 500000;
66
+ readonly isDefault: false;
67
+ }];
@@ -0,0 +1,102 @@
1
+ import type { CredentialRef, CredentialStore, ProviderAdapter } from "../../types.js";
2
+ import { type ChatTurnOptions } from "./chat-completions.js";
3
+ /** The registered adapter id. One provider, so it is a constant rather than an option. */
4
+ export declare const XAI_OAUTH_ADAPTER_ID = "winter.xai-oauth";
5
+ /**
6
+ * The flow's constants, as SHIPPED.
7
+ *
8
+ * Stated as independent literals rather than read out of `DERIVED_XAI`, so that the constants test
9
+ * compares two things instead of one thing with itself. If you change a value here, re-run the
10
+ * capture and change `xai-derived-shapes.ts` too — the test is there to make that unavoidable.
11
+ */
12
+ export declare const XAI_OAUTH: {
13
+ /** xAI's public client. Secret-less: the authorization server advertises `none` among its token-endpoint auth methods. */
14
+ readonly clientId: "b1a00492-073a-47ea-816f-4c329264a828";
15
+ /** The vendor client's own frozen ten-scope set, unchanged (capture §5.3 records why it is not narrowed, and what the live gate should try). */
16
+ readonly scope: "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write workspaces:read workspaces:write";
17
+ /** The form field this flow carries a client identity in. */
18
+ readonly identityField: "referrer";
19
+ /**
20
+ * DOCUMENTATION ONLY as of P7a (fix r1, Minor-3): the DEFAULT profile's token, kept so this row
21
+ * still SHOWS what it sends. Nothing reads it — all three wire sites below read
22
+ * `activeWinterIdentity().product`, which is the running brand's own name.
23
+ *
24
+ * What it documents is the whole of what makes this row admissible rather than impersonation: the
25
+ * bare product name (not `winterUserAgent()`'s versioned token) in an originator field a vendor's
26
+ * analytics buckets, the same shape as codex's own `originator`. It is never the vendor's value,
27
+ * and it is never omitted — see the reversion condition below for what it means if that turns out
28
+ * not to be allowed. A future edit here changes NOTHING on the wire; change the brand profile.
29
+ */
30
+ readonly identityValue: string;
31
+ readonly deviceCodeUrl: "https://auth.x.ai/oauth2/device/code";
32
+ readonly tokenUrl: "https://auth.x.ai/oauth2/token";
33
+ /**
34
+ * Where a subscription bearer is actually spent.
35
+ *
36
+ * NOT `https://api.x.ai/v1`. That is xAI's METERED, api-key surface and it belongs to the separate
37
+ * token-priced `xai` row. This one is the proxy the vendor's own installers default to, the host
38
+ * its 401s report `auth_kind=bearer` from, and the one the `grok-cli:access` scope is described as
39
+ * authorizing. Pointing a subscription token at the metered endpoint either fails to authenticate
40
+ * or bills the user for traffic their subscription already covers.
41
+ */
42
+ readonly apiBaseUrl: "https://cli-chat-proxy.grok.com/v1";
43
+ };
44
+ /**
45
+ * What a host must tell the user on the connect screen, before the browser opens.
46
+ *
47
+ * Audit §2.5's disclosure note. Because the flow rides xAI's shared public client, the consent page
48
+ * may name the vendor's own product rather than Winter. That is structurally identical to Codex
49
+ * OAuth and is not impersonation by Winter — Winter names itself in the flow and on every request —
50
+ * but it IS a user-facing misattribution, and a user who is not told will reasonably read it as one.
51
+ * Exported rather than hard-coded into a UI so every host says the same thing.
52
+ */
53
+ export declare const XAI_CONSENT_DISCLOSURE: string;
54
+ /** How an xAI credential is stored: one `oauth` material under `xai-oauth:<accountId>` (R6-10, one record per provider/account). */
55
+ export declare function xaiCredentialRef(accountId: string, service?: string): Extract<CredentialRef, {
56
+ kind: "keychain";
57
+ }>;
58
+ export interface XaiLoginOptions {
59
+ /** Overridden by a fixture; production uses `XAI_OAUTH`'s own values. */
60
+ deviceCodeUrl?: string;
61
+ tokenUrl?: string;
62
+ pollIntervalMs?: number;
63
+ timeoutMs?: number;
64
+ /** The Keychain service the record lands in — `config.keychainService` from the host. */
65
+ service?: string;
66
+ onAuthStatus?: (status: {
67
+ isAuthenticating: boolean;
68
+ output?: string[];
69
+ error?: string;
70
+ }) => void;
71
+ }
72
+ export interface XaiLoginResult {
73
+ ref: Extract<CredentialRef, {
74
+ kind: "keychain";
75
+ }>;
76
+ accountId: string;
77
+ expiresAt: number;
78
+ }
79
+ /**
80
+ * The host-invoked login. Runs the device authorization grant and PERSISTS the result through the
81
+ * credential store, returning the ref a session should be configured with.
82
+ *
83
+ * The identity field rides the device request and every poll (the shared helper's rule, and a
84
+ * deliberate superset of what the vendor's own client does — capture §5.1). There is no code path
85
+ * here that retries without it.
86
+ */
87
+ export declare function startXaiLogin(store: CredentialStore, options?: XaiLoginOptions): Promise<XaiLoginResult>;
88
+ export interface XaiAdapterOptions extends Omit<ChatTurnOptions, "generatedBaseUrl" | "organization" | "project"> {
89
+ generatedBaseUrl?: string;
90
+ /** Overridden by a fixture; production uses `XAI_OAUTH.tokenUrl`. */
91
+ tokenUrl?: string;
92
+ }
93
+ /**
94
+ * `winter.xai-oauth` — the chat adapter, at xAI's subscription proxy, on an oauth bearer.
95
+ *
96
+ * A composition rather than a copy, and deliberately so: everything about the TURN is
97
+ * `openai-chat-completions@1`'s, and `resolveAuth` already turns `oauth` material into
98
+ * `Authorization: Bearer <accessToken>`. What differs is the endpoint, the id the registry resolves
99
+ * it by, and the refresh above — so only those are restated. Copying the turn would have given this
100
+ * row its own drifting version of streaming, tool-call mapping and error classification for no gain.
101
+ */
102
+ export declare function createXaiOauthAdapter(options: XaiAdapterOptions): ProviderAdapter;
@@ -0,0 +1,62 @@
1
+ /** Only what an assertion needs. Header names are lowercased; an `authorization` value would be redacted, though these two endpoints carry none. */
2
+ export interface XaiRecordedRequest {
3
+ method: string;
4
+ path: string;
5
+ headers: Record<string, string>;
6
+ /** Raw form body. Bodies are not redacted: the body is exactly what the identity assertions are about. */
7
+ body: string;
8
+ }
9
+ export interface XaiOauthFake {
10
+ deviceCodeUrl: string;
11
+ tokenUrl: string;
12
+ requests: XaiRecordedRequest[];
13
+ close(): Promise<void>;
14
+ }
15
+ export interface XaiChatFake {
16
+ /** The base URL a `generatedBaseUrl` points at. */
17
+ url: string;
18
+ requests: XaiRecordedRequest[];
19
+ /**
20
+ * Whether the bearer this fake actually received was the REFRESHED one.
21
+ *
22
+ * Observed here rather than asserted from the recorded header, because `requests` redacts an
23
+ * `authorization` value to `Bearer ***` — which makes "the turn carried the new token" untestable
24
+ * from the outside. Comparing inside the fake and exposing one boolean keeps the token out of the
25
+ * transcript while still making the claim falsifiable.
26
+ */
27
+ sawFreshBearer: boolean;
28
+ close(): Promise<void>;
29
+ }
30
+ export interface XaiOauthFakeOptions {
31
+ /**
32
+ * When the identity field arrives carrying THIS value, the token endpoint answers `access_denied`
33
+ * — the vendor refusing an agent it does not recognise.
34
+ *
35
+ * It answers at the TOKEN endpoint, not the device endpoint, and that is deliberate rather than
36
+ * convenient: RFC 8628 §3.5 is where `access_denied` lives, the device-authorization response
37
+ * (§3.2) carries the RFC 6749 §5.2 set instead, and a real authorization server decides a consent
38
+ * question when the authorization is finalized rather than when a code is minted. The device
39
+ * endpoint still validates the field — it refuses a request that OMITS it (see below).
40
+ */
41
+ rejectIdentity?: string;
42
+ /** How many polls answer `authorization_pending` before the token arrives. Default 0. */
43
+ pendingPolls?: number;
44
+ /** An ordinary terminal failure from the token endpoint (`expired_token`, …) — the negative that keeps `rejectIdentity` meaningful. */
45
+ tokenError?: string;
46
+ /** Omit the id token from the successful response, so the caller has no account id to name a record with (R6-10). */
47
+ omitIdToken?: boolean;
48
+ }
49
+ /** What the token endpoint answers a `refresh_token` grant with. Named so the chat fake and the tests agree on it without re-spelling the literal. */
50
+ export declare const REFRESHED_ACCESS_TOKEN = "test-token-xai-access-refreshed";
51
+ /**
52
+ * A minimal `/chat/completions` double, so a REFRESH test can prove the turn that followed carried
53
+ * the new token.
54
+ *
55
+ * Deliberately tiny rather than a second copy of the conformance chat fake: the only questions asked
56
+ * of it are "did a turn happen" and "what did it carry", and the full corpus fake lives on the other
57
+ * side of a dependency direction this package cannot import across.
58
+ */
59
+ export declare function startXaiChatFake(opts?: {
60
+ freshBearer?: string;
61
+ }): Promise<XaiChatFake>;
62
+ export declare function startXaiOauthFake(opts?: XaiOauthFakeOptions): Promise<XaiOauthFake>;