@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.
- package/LICENSE +21 -0
- package/NOTICE +41 -0
- package/README.md +109 -0
- package/dist/adapters/anthropic/console-oauth.d.ts +101 -0
- package/dist/adapters/anthropic/index.d.ts +4 -0
- package/dist/adapters/anthropic/messages.d.ts +76 -0
- package/dist/adapters/bedrock/converse.d.ts +143 -0
- package/dist/adapters/bedrock/crc32.d.ts +9 -0
- package/dist/adapters/bedrock/credentials.d.ts +32 -0
- package/dist/adapters/bedrock/eventstream.d.ts +65 -0
- package/dist/adapters/bedrock/index.d.ts +8 -0
- package/dist/adapters/bedrock/sigv4.d.ts +119 -0
- package/dist/adapters/bedrock/testing.d.ts +46 -0
- package/dist/adapters/content-blocks.d.ts +7 -0
- package/dist/adapters/google/adc.d.ts +35 -0
- package/dist/adapters/google/generate-content.d.ts +136 -0
- package/dist/adapters/google/index.d.ts +8 -0
- package/dist/adapters/google/jwt-rs256.d.ts +36 -0
- package/dist/adapters/google/vertex.d.ts +15 -0
- package/dist/adapters/index.d.ts +34 -0
- package/dist/adapters/oauth/device-code.d.ts +32 -0
- package/dist/adapters/oauth/refresh.d.ts +40 -0
- package/dist/adapters/openai/azure.d.ts +38 -0
- package/dist/adapters/openai/chat-completions.d.ts +86 -0
- package/dist/adapters/openai/codex-config.d.ts +42 -0
- package/dist/adapters/openai/codex-oauth.d.ts +47 -0
- package/dist/adapters/openai/index.d.ts +20 -0
- package/dist/adapters/openai/local.d.ts +16 -0
- package/dist/adapters/openai/pkce.d.ts +111 -0
- package/dist/adapters/openai/quota.d.ts +99 -0
- package/dist/adapters/openai/responses.d.ts +142 -0
- package/dist/adapters/openai/shared.d.ts +359 -0
- package/dist/adapters/openai/testing.d.ts +59 -0
- package/dist/adapters/openai/xai-derived-shapes.d.ts +67 -0
- package/dist/adapters/openai/xai-oauth.d.ts +102 -0
- package/dist/adapters/openai/xai-oauth.testing.d.ts +62 -0
- package/dist/adapters/privileged-headers.d.ts +51 -0
- package/dist/adapters/refusals.d.ts +10 -0
- package/dist/address-classifier.d.ts +17 -0
- package/dist/bun-required.d.ts +54 -0
- package/dist/continuity/decoration.d.ts +89 -0
- package/dist/continuity/domains.d.ts +92 -0
- package/dist/continuity/fixtures.d.ts +44 -0
- package/dist/continuity/handoff.d.ts +94 -0
- package/dist/continuity/index.d.ts +10 -0
- package/dist/continuity/renderer.d.ts +111 -0
- package/dist/continuity/warnings.d.ts +46 -0
- package/dist/credentials/env.d.ts +6 -0
- package/dist/credentials/file.d.ts +21 -0
- package/dist/credentials/memory.d.ts +8 -0
- package/dist/credentials/types.d.ts +38 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/endpoint-policy.d.ts +127 -0
- package/dist/errors.d.ts +68 -0
- package/dist/http.d.ts +29 -0
- package/dist/identity.d.ts +62 -0
- package/dist/index-5z94gxhk.js +43790 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +3194 -0
- package/dist/registry.d.ts +136 -0
- package/dist/retry.d.ts +38 -0
- package/dist/sse.d.ts +12 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.js +432 -0
- package/dist/types.d.ts +376 -0
- 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>;
|