@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,42 @@
|
|
|
1
|
+
export declare const CODEX: {
|
|
2
|
+
/** OAuth application id — shared by all Codex CLI clients. */
|
|
3
|
+
readonly clientId: "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
4
|
+
/** Authorization endpoint: `{issuer}/oauth/authorize`. */
|
|
5
|
+
readonly authorizeUrl: "https://auth.openai.com/oauth/authorize";
|
|
6
|
+
/** Token exchange + refresh endpoint. */
|
|
7
|
+
readonly tokenUrl: "https://auth.openai.com/oauth/token";
|
|
8
|
+
/** Local callback port (primary). */
|
|
9
|
+
readonly callbackPort: 1455;
|
|
10
|
+
/** The fallback port, per codex-rs's own allow-list. A login refuses rather than drifting onto an arbitrary port. */
|
|
11
|
+
readonly fallbackCallbackPort: 1457;
|
|
12
|
+
/** OAuth scopes — codex-rs's own scope string from `build_authorize_url`. */
|
|
13
|
+
readonly scope: "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
14
|
+
/** Base URL for the ChatGPT Codex backend; `/responses` is appended for a turn. */
|
|
15
|
+
readonly backendUrl: "https://chatgpt.com/backend-api/codex";
|
|
16
|
+
readonly headers: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
/** The DEFAULT `originator` value, exported so a fixture asserts on it by name rather than by string literal. */
|
|
19
|
+
export declare const CODEX_ORIGINATOR: string;
|
|
20
|
+
/**
|
|
21
|
+
* The date `CODEX_MODELS` was last checked against the live `/models` catalogue, carried from the
|
|
22
|
+
* port. It lives next to the data it dates so the two cannot drift apart.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CODEX_MODELS_VERIFIED = "2026-07-31";
|
|
25
|
+
/**
|
|
26
|
+
* The static Codex model set.
|
|
27
|
+
*
|
|
28
|
+
* The backend returns only these slugs for ChatGPT-account auth; other OpenAI model ids answer
|
|
29
|
+
* HTTP 400 ("not supported when using Codex with a ChatGPT account"), so this is an ALLOW-LIST in
|
|
30
|
+
* effect and not merely a convenience. `contextWindow` is 272000 for every model the catalogue
|
|
31
|
+
* offers — a number the port records as having been WRONG once (372000, hand-transcribed), which
|
|
32
|
+
* put an auto-compaction threshold above the backend's own hard ceiling and killed compaction
|
|
33
|
+
* silently. Re-derive it from the live catalogue; never edit it by hand.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CODEX_MODELS: ReadonlyArray<{
|
|
36
|
+
id: string;
|
|
37
|
+
contextWindow: number;
|
|
38
|
+
supportsVision: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
export declare const DEFAULT_CODEX_MODEL = "gpt-5.6-sol";
|
|
41
|
+
/** The Keychain account a codex credential occupies: `codex-oauth:<accountId>` (R6-10's one-record-per-provider/account rule). */
|
|
42
|
+
export declare function codexCredentialAccount(accountId: string): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CredentialRef, CredentialStore, ProviderAdapter } from "../../types.js";
|
|
2
|
+
import { QuotaManager } from "./quota.js";
|
|
3
|
+
import { type OpenAiAdapterOptions } from "./shared.js";
|
|
4
|
+
export interface CodexAdapterOptions extends OpenAiAdapterOptions {
|
|
5
|
+
/** The token endpoint. Injectable for a fixture; the production value is `CODEX.tokenUrl`. */
|
|
6
|
+
tokenUrl?: string;
|
|
7
|
+
/** Shared across turns so the subscription state survives one. A fixture may inject its own clock. */
|
|
8
|
+
quota?: QuotaManager;
|
|
9
|
+
}
|
|
10
|
+
/** How a codex credential is stored: one `oauth` material under `codex-oauth:<accountId>`. */
|
|
11
|
+
export declare function codexCredentialRef(accountId: string, service?: string): Extract<CredentialRef, {
|
|
12
|
+
kind: "keychain";
|
|
13
|
+
}>;
|
|
14
|
+
export interface CodexLoginOptions {
|
|
15
|
+
openUrl: (url: string) => Promise<void>;
|
|
16
|
+
/** Overridden by a fixture; production uses `CODEX`'s own values. */
|
|
17
|
+
authorizeUrl?: string;
|
|
18
|
+
tokenUrl?: string;
|
|
19
|
+
callbackPort?: number;
|
|
20
|
+
fallbackCallbackPort?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** The Keychain service the record lands in — `config.keychainService` from the host. */
|
|
23
|
+
service?: string;
|
|
24
|
+
onAuthStatus?: (status: {
|
|
25
|
+
isAuthenticating: boolean;
|
|
26
|
+
output?: string[];
|
|
27
|
+
error?: string;
|
|
28
|
+
}) => void;
|
|
29
|
+
}
|
|
30
|
+
export interface CodexLoginResult {
|
|
31
|
+
ref: Extract<CredentialRef, {
|
|
32
|
+
kind: "keychain";
|
|
33
|
+
}>;
|
|
34
|
+
accountId: string;
|
|
35
|
+
expiresAt: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The host-invoked login. Runs the PKCE loopback flow and PERSISTS the result through the
|
|
39
|
+
* credential store, returning the ref a session should be configured with.
|
|
40
|
+
*
|
|
41
|
+
* An account id is required to name the record: without one there is no per-account slot, and R6-10
|
|
42
|
+
* is explicit that a credential occupies one record per provider/account rather than a shared global
|
|
43
|
+
* slot. A token exchange that returned no id token is therefore a refusal, not a fallback to some
|
|
44
|
+
* default name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function startCodexLogin(store: CredentialStore, options: CodexLoginOptions): Promise<CodexLoginResult>;
|
|
47
|
+
export declare function createCodexOauthAdapter(options: CodexAdapterOptions): ProviderAdapter;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { createResponsesAdapter, buildResponsesBody, mapResponsesInput, mapResponsesTools, responsesTurn, streamResponsesTurn, privilegedHeaders, ResponsesStreamMapper, OPENAI_API_BASE_URL } from "./responses.js";
|
|
2
|
+
export type { ResponsesTurnPlan } from "./responses.js";
|
|
3
|
+
export { createChatCompletionsAdapter, buildChatBody, mapChatMessages, mapChatTools, chatTurn, deepSeekProfile, openRouterProfile, ChatStreamMapper, DEEPSEEK_BASE_URL, OPENAI_CHAT_BASE_URL, OPENROUTER_BASE_URL, } from "./chat-completions.js";
|
|
4
|
+
export type { ChatTurnOptions, ExposedReasoningItem } from "./chat-completions.js";
|
|
5
|
+
export { createCodexOauthAdapter, codexCredentialRef, startCodexLogin } from "./codex-oauth.js";
|
|
6
|
+
export type { CodexAdapterOptions, CodexLoginOptions, CodexLoginResult } from "./codex-oauth.js";
|
|
7
|
+
export { CODEX, CODEX_MODELS, CODEX_MODELS_VERIFIED, CODEX_ORIGINATOR, DEFAULT_CODEX_MODEL, codexCredentialAccount } from "./codex-config.js";
|
|
8
|
+
export { QuotaManager, quotaEvent } from "./quota.js";
|
|
9
|
+
export type { QuotaState } from "./quota.js";
|
|
10
|
+
export { base64Url, buildAuthorizeUrl, decodeAccountId, generatePkce, refreshTokens, runLoginFlow } from "./pkce.js";
|
|
11
|
+
export type { LoginConfig, OAuthTokens } from "./pkce.js";
|
|
12
|
+
export { XAI_CONSENT_DISCLOSURE, XAI_OAUTH, XAI_OAUTH_ADAPTER_ID, createXaiOauthAdapter, startXaiLogin, xaiCredentialRef } from "./xai-oauth.js";
|
|
13
|
+
export type { XaiLoginOptions, XaiLoginResult } from "./xai-oauth.js";
|
|
14
|
+
export { DERIVED_XAI, DERIVED_XAI_COMMIT, DERIVED_XAI_MODELS } from "./xai-derived-shapes.js";
|
|
15
|
+
export { createLocalOpenAIAdapter } from "./local.js";
|
|
16
|
+
export type { LocalAdapterOptions } from "./local.js";
|
|
17
|
+
export { createAzureOpenAIAdapter, azureProfile, azureRouting, azureTurnUrl, AZURE_PREVIEW_API_VERSION } from "./azure.js";
|
|
18
|
+
export type { AzureAdapterOptions } from "./azure.js";
|
|
19
|
+
export { EFFORT_LADDER, EventQueue, assertRepresentableTools, assertWithinLimits, buildHeaders, capabilitiesFrom, capabilityRefusal, fetchOpenAiModels, mapEffortAgainst, pumpEvents, resolveEndpoint, resolveReasoning, snapNumericEffort, } from "./shared.js";
|
|
20
|
+
export type { AuthStyle, DescriptorLookup, OpenAiAdapterOptions, ReasoningPlan, ResolvedEndpoint } from "./shared.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProviderAdapter } from "../../types.js";
|
|
2
|
+
import { type ChatTurnOptions } from "./chat-completions.js";
|
|
3
|
+
export interface LocalAdapterOptions extends ChatTurnOptions {
|
|
4
|
+
/** Which surface this server speaks. Almost every local server is `chat`; a few expose Responses. */
|
|
5
|
+
surface?: "chat" | "responses";
|
|
6
|
+
/**
|
|
7
|
+
* The registered adapter id.
|
|
8
|
+
*
|
|
9
|
+
* Overridable because the catalog's twelve local providers currently name
|
|
10
|
+
* `winter.openai-chat-completions` as their `adapterId`, and a registry resolves an adapter BY
|
|
11
|
+
* THAT ID. A host that wants the `/api/tags` fallback registers this adapter under whichever id
|
|
12
|
+
* its catalog rows actually point at.
|
|
13
|
+
*/
|
|
14
|
+
id?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createLocalOpenAIAdapter(options: LocalAdapterOptions): ProviderAdapter;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface OAuthTokens {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
idToken?: string;
|
|
5
|
+
accountId?: string;
|
|
6
|
+
/** Epoch milliseconds. */
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginConfig {
|
|
10
|
+
clientId: string;
|
|
11
|
+
authorizeUrl: string;
|
|
12
|
+
tokenUrl: string;
|
|
13
|
+
/** 1455 in production (codex-rs parity); `0` gives an ephemeral port, which is what a fixture uses. */
|
|
14
|
+
callbackPort: number;
|
|
15
|
+
/** Tried when `callbackPort` is already taken. Omitted for an ephemeral port. */
|
|
16
|
+
fallbackCallbackPort?: number;
|
|
17
|
+
/**
|
|
18
|
+
* The loopback callback's PATH, which is part of what a registered `redirect_uri` matches.
|
|
19
|
+
*
|
|
20
|
+
* P6.5 lane A2: this was a hard-coded `/auth/callback` when codex was the only flow, and the
|
|
21
|
+
* Anthropic Console client registers `/callback` instead (derived-shapes-p6b.md §2.2). Optional
|
|
22
|
+
* and defaulted, so codex's own value is unchanged and no existing caller sees a difference.
|
|
23
|
+
*/
|
|
24
|
+
callbackPath?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Names the flow in this file's user-facing failures ("the … login timed out").
|
|
27
|
+
*
|
|
28
|
+
* Defaulted to codex for the same reason: those strings were written when there was one flow, and
|
|
29
|
+
* an Anthropic user told their sign-in failed because "the codex login timed out" would be reading
|
|
30
|
+
* a message about a product they are not using.
|
|
31
|
+
*/
|
|
32
|
+
label?: string;
|
|
33
|
+
/**
|
|
34
|
+
* P7a pre-publish (N1): the EXPORTED function a caller invoked, for `BunRequiredError.functionName`.
|
|
35
|
+
*
|
|
36
|
+
* Separate from `label` on purpose, because the two answer different questions and round 3
|
|
37
|
+
* conflated them. `label` is USER-FACING PROSE ("the Anthropic Console login timed out") and its
|
|
38
|
+
* own doc above reserves it for that; `functionName` is a DEVELOPER-FACING identifier that must be
|
|
39
|
+
* the symbol in the caller's code (`bun-required.ts`: "never the internal helper that reaches for
|
|
40
|
+
* Bun"). Round 3 fixed the identifier by writing it into `label`, which made a codex user read
|
|
41
|
+
* "the startCodexLogin login timed out" -- a function name where a product name goes.
|
|
42
|
+
*
|
|
43
|
+
* Absent -> the guard names `runLoginFlow`, which is honest for a direct caller of this helper.
|
|
44
|
+
*/
|
|
45
|
+
functionName?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The token exchange's body encoding. Defaults to `"form"`.
|
|
48
|
+
*
|
|
49
|
+
* P6.5 ruling R-A2-1 — see `adapters/oauth/refresh.ts` for the whole reasoning. Short version:
|
|
50
|
+
* RFC 6749 §4.1.3 requires a token endpoint to accept form encoding, but the Anthropic Console
|
|
51
|
+
* endpoint is only ever OBSERVED receiving JSON, and guessing wrong there breaks the login
|
|
52
|
+
* outright. The default keeps codex byte-identical.
|
|
53
|
+
*/
|
|
54
|
+
bodyEncoding?: "form" | "json";
|
|
55
|
+
/**
|
|
56
|
+
* Whether the authorization-code grant carries `state`. Defaults to `false`.
|
|
57
|
+
*
|
|
58
|
+
* `state` is NOT an RFC 6749 token-request parameter — it is the authorize-request nonce echoed
|
|
59
|
+
* back on the redirect, and this flow has already compared it before accepting the code. The
|
|
60
|
+
* Anthropic Console client sends it on the grant anyway, so Winter matches that endpoint's observed
|
|
61
|
+
* request; every other flow must not, because sending a parameter a vendor never sees from its own
|
|
62
|
+
* client is a gratuitous difference on the one request whose failure mode is "your login is
|
|
63
|
+
* broken". It was briefly unconditional and the controller reverted it: codex's token request is
|
|
64
|
+
* byte-identical to what it was before this lane, and a fixture pins the exact body to keep it so.
|
|
65
|
+
*/
|
|
66
|
+
includeStateInTokenRequest?: boolean;
|
|
67
|
+
scope: string;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
/** Opens the browser. HOST-supplied: the SDK never shells out to a browser itself. */
|
|
70
|
+
openUrl: (url: string) => Promise<void>;
|
|
71
|
+
onAuthStatus?: (status: {
|
|
72
|
+
isAuthenticating: boolean;
|
|
73
|
+
output?: string[];
|
|
74
|
+
error?: string;
|
|
75
|
+
}) => void;
|
|
76
|
+
}
|
|
77
|
+
/** base64url over raw bytes, no padding. Hand-rolled because `btoa` needs a binary string and the padding has to go. */
|
|
78
|
+
export declare function base64Url(bytes: Uint8Array): string;
|
|
79
|
+
/** A PKCE verifier/challenge pair. 48 random bytes is 64 base64url characters — inside RFC 7636's 43-128 range. */
|
|
80
|
+
export declare function generatePkce(): Promise<{
|
|
81
|
+
verifier: string;
|
|
82
|
+
challenge: string;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Reads the ChatGPT account id out of an id token WITHOUT verifying it.
|
|
86
|
+
*
|
|
87
|
+
* That is safe for exactly one reason, and only that one: the value is used as a LOCATOR — the
|
|
88
|
+
* Keychain account name and a request header — never as an authorization decision. The token came
|
|
89
|
+
* from a TLS-authenticated token endpoint over a PKCE-bound exchange; nothing here treats a claim
|
|
90
|
+
* in it as a permission.
|
|
91
|
+
*/
|
|
92
|
+
export declare function decodeAccountId(idToken: string): string | undefined;
|
|
93
|
+
export declare function refreshTokens(tokenUrl: string, clientId: string, refreshToken: string): Promise<OAuthTokens>;
|
|
94
|
+
/**
|
|
95
|
+
* Runs the loopback authorization-code flow.
|
|
96
|
+
*
|
|
97
|
+
* The callback server binds `127.0.0.1` ONLY: an authorization code is a single-use credential, and
|
|
98
|
+
* a server on `0.0.0.0` would make it reachable from the LAN for as long as the login is open.
|
|
99
|
+
* `state` is compared before the code is accepted, which is what makes a CSRF-planted callback fail
|
|
100
|
+
* instead of completing someone else's login into this process.
|
|
101
|
+
*/
|
|
102
|
+
export declare function runLoginFlow(cfg: LoginConfig): Promise<OAuthTokens>;
|
|
103
|
+
/** The authorize URL a login would open, for a fixture that wants to drive the callback without a browser. */
|
|
104
|
+
export declare function buildAuthorizeUrl(cfg: {
|
|
105
|
+
authorizeUrl: string;
|
|
106
|
+
clientId: string;
|
|
107
|
+
redirectUri: string;
|
|
108
|
+
scope: string;
|
|
109
|
+
state: string;
|
|
110
|
+
challenge: string;
|
|
111
|
+
}): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ProviderEvent } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `resumeAt` is OPTIONAL, and that is the whole of finding I1's fix.
|
|
4
|
+
*
|
|
5
|
+
* A 429 without a `Retry-After` header is a limit whose WINDOW IS UNKNOWN — the account is refused,
|
|
6
|
+
* and nothing has said when it resumes. Modelling that as `resumeAt: <some number>` forced the
|
|
7
|
+
* caller to invent one (the previous code handed it Winter's own jittered backoff, which then rode
|
|
8
|
+
* the pinned `SDKRateLimitInfo.resetsAt` as a claim about the subscription), and modelling it as
|
|
9
|
+
* `resumeAt: 0` made it indistinguishable from "not limited". So `limited` is tracked as its own
|
|
10
|
+
* flag, independent of any window, and a window-unknown limit omits `resetsAt` entirely.
|
|
11
|
+
*/
|
|
12
|
+
export type QuotaState = {
|
|
13
|
+
kind: "ok";
|
|
14
|
+
} | {
|
|
15
|
+
kind: "limited";
|
|
16
|
+
resumeAt?: number;
|
|
17
|
+
};
|
|
18
|
+
export interface QuotaManagerOptions {
|
|
19
|
+
maxConcurrent?: number;
|
|
20
|
+
/** Injected so a fixture never waits out a real limit window. */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
/**
|
|
23
|
+
* How `waitIfLimited` waits. Injectable for the same reason `RetryPolicyOptions.sleep` is, and it
|
|
24
|
+
* is NOT redundant with it: in production the retry backoff and the quota window are the same
|
|
25
|
+
* wait (the backoff consumes the window), but a fixture that mocks only the retry sleep still
|
|
26
|
+
* spends the real window here — twice per 429 scenario.
|
|
27
|
+
*/
|
|
28
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Tracks a ChatGPT account's quota state and caps concurrent turns against it.
|
|
32
|
+
*
|
|
33
|
+
* `resumeAt` is epoch MILLISECONDS internally and is rendered as epoch SECONDS on the event, which
|
|
34
|
+
* is the pinned `SDKRateLimitInfo.resetsAt` convention. Only fields the pin actually declares are
|
|
35
|
+
* emitted: `status` and `resetsAt`. Inventing a `rateLimitType` (whose six members are all
|
|
36
|
+
* consumer-subscription shapes) would be a claim about which window was hit, and the backend's 429
|
|
37
|
+
* does not say.
|
|
38
|
+
*/
|
|
39
|
+
export declare class QuotaManager {
|
|
40
|
+
private readonly maxConcurrent;
|
|
41
|
+
private readonly now;
|
|
42
|
+
private readonly sleep;
|
|
43
|
+
private active;
|
|
44
|
+
private waiters;
|
|
45
|
+
/** Set by a refusal, cleared only by `noteRecovered()`. Survives the window elapsing — see `hasPendingLimit`. */
|
|
46
|
+
private limited;
|
|
47
|
+
/** `0` means "limited, window unknown". Never a synthesised value. */
|
|
48
|
+
private limitedUntil;
|
|
49
|
+
private listeners;
|
|
50
|
+
private totals;
|
|
51
|
+
constructor(opts?: QuotaManagerOptions);
|
|
52
|
+
/**
|
|
53
|
+
* The state a `rate_limit` event is rendered from.
|
|
54
|
+
*
|
|
55
|
+
* A KNOWN window that has elapsed reads `ok` — the account is presumed serving again, which is
|
|
56
|
+
* what the backend told us. A window-UNKNOWN limit stays `limited` until something says otherwise,
|
|
57
|
+
* because nothing has: guessing that it lapsed would be the same invention `resumeAt` refuses.
|
|
58
|
+
*/
|
|
59
|
+
state(): QuotaState;
|
|
60
|
+
/**
|
|
61
|
+
* True from a refusal until `noteRecovered()`, WHATEVER the clock says.
|
|
62
|
+
*
|
|
63
|
+
* `state()` cannot answer this (finding I2): by the time a retried turn completes, the retry
|
|
64
|
+
* backoff has slept the whole window, so `state()` reads `ok` and "was this turn rate-limited?"
|
|
65
|
+
* silently becomes "no" — which made the recovery event unreachable under a real clock while a
|
|
66
|
+
* fixture with a 1 ms mocked sleep passed.
|
|
67
|
+
*/
|
|
68
|
+
hasPendingLimit(): boolean;
|
|
69
|
+
onStateChange(callback: (state: QuotaState) => void): () => void;
|
|
70
|
+
private emit;
|
|
71
|
+
/**
|
|
72
|
+
* Records that the backend refused for quota reasons.
|
|
73
|
+
*
|
|
74
|
+
* `retryAfterMs` MUST come from the refused response's own `Retry-After` header, never from a
|
|
75
|
+
* computed backoff (finding I1). Absent means the window is unknown, which is recorded as exactly
|
|
76
|
+
* that: limited, with no resume time.
|
|
77
|
+
*/
|
|
78
|
+
noteRateLimit(retryAfterMs: number | undefined): void;
|
|
79
|
+
/** The account is serving again. Silent when it never stopped — a listener must not see a state change that did not happen. */
|
|
80
|
+
noteRecovered(): void;
|
|
81
|
+
accumulate(inputTokens: number, outputTokens: number): void;
|
|
82
|
+
usage(): {
|
|
83
|
+
inputTokens: number;
|
|
84
|
+
outputTokens: number;
|
|
85
|
+
};
|
|
86
|
+
acquire(): Promise<void>;
|
|
87
|
+
release(): void;
|
|
88
|
+
/** Waits out a known limit window, ABORTABLY: an interrupt must not have to outlast a quota reset. */
|
|
89
|
+
waitIfLimited(signal?: AbortSignal): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A quota state -> the pinned-shaped `rate_limit` event.
|
|
93
|
+
*
|
|
94
|
+
* `kind: "subscription-quota"` is the type-level statement that this is NOT an HTTP 429 (R6-B), and
|
|
95
|
+
* it is the only `kind` the seam admits.
|
|
96
|
+
*/
|
|
97
|
+
export declare function quotaEvent(state: QuotaState): Extract<ProviderEvent, {
|
|
98
|
+
type: "rate_limit";
|
|
99
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { WinterModelDescriptor } from "@yanlinglabs/winter-provider-catalog";
|
|
2
|
+
import type { ProviderAdapter, ProviderContext, ProviderEvent, ProviderMessageLike, TurnRequest } from "../../types.js";
|
|
3
|
+
import { EventQueue, type OpenAiAdapterOptions, type ReasoningPlan, type ResolvedEndpoint } from "./shared.js";
|
|
4
|
+
export declare const OPENAI_API_BASE_URL = "https://api.openai.com/v1";
|
|
5
|
+
/**
|
|
6
|
+
* `ProviderMessageLike[]` -> the Responses `input` array.
|
|
7
|
+
*
|
|
8
|
+
* The STRUCTURED content form is mandatory, and that is a live finding rather than a style choice:
|
|
9
|
+
* the flat `{ role, content: "string" }` form was rejected with an HTTP 400 by the codex backend
|
|
10
|
+
* (Norma, 2026-06-13). Assistant content is `output_text`, everything else `input_text`.
|
|
11
|
+
*
|
|
12
|
+
* NATIVE STATE LEADS ITS MESSAGE. §5.3 requires the provider's completed output items to be replayed
|
|
13
|
+
* in their original order among messages and tool calls; a turn's real order is reasoning item(s)
|
|
14
|
+
* first, then the message / function_call it produced. The renderer has already dropped any state
|
|
15
|
+
* from a foreign continuation domain before this sees it, so what arrives here is replayable by
|
|
16
|
+
* construction.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mapResponsesInput(messages: readonly ProviderMessageLike[]): unknown[];
|
|
19
|
+
export declare function mapResponsesTools(tools: TurnRequest["tools"]): unknown[];
|
|
20
|
+
/**
|
|
21
|
+
* The Responses request body.
|
|
22
|
+
*
|
|
23
|
+
* `instructions` is sent ONLY when the caller supplied a non-empty system prompt — a deliberate
|
|
24
|
+
* deviation from the port, which sent a default string. Global Constraints forbid vendor prompt
|
|
25
|
+
* text, and inventing a Winter one would put an instruction in front of the model that no caller
|
|
26
|
+
* asked for; codex-rs itself skips the field when it is empty.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildResponsesBody(req: TurnRequest, reasoning: ReasoningPlan, descriptor: WinterModelDescriptor | undefined): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Responses SSE frames -> Winter's normalized `ProviderEvent`s.
|
|
31
|
+
*
|
|
32
|
+
* One instance per turn: the reasoning-item accumulator, the seen-a-call flag and the
|
|
33
|
+
* did-this-call-stream-its-arguments map are all per-turn state.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Which stream event this family treats as COMPLETING, from the descriptor's own `completionEvent`
|
|
37
|
+
* evidence (review round 1, C — ledger line 36).
|
|
38
|
+
*
|
|
39
|
+
* Lane B's pattern, applied here: `anthropicCaptureEvent` and the Google resolver both read this
|
|
40
|
+
* field, and this family hard-coded `"response.completed"` — so a catalog row that DECLARED a
|
|
41
|
+
* different terminator was silently ignored, and the evidence existed for two families out of four.
|
|
42
|
+
*
|
|
43
|
+
* MATCHED LENIENTLY BY MENTION, for the same reason those two are: the field is a prose-ish
|
|
44
|
+
* `CapabilityEvidence<string>`, not an enum. `"response.completed"` stays as the fallback and is the
|
|
45
|
+
* conservative answer — the pinned terminator for this surface, and the one every unlisted row means.
|
|
46
|
+
*
|
|
47
|
+
* `response.incomplete` is always ALSO treated as terminal and is deliberately not configurable: it
|
|
48
|
+
* is a truncation, not a completion, and a row that named some other event would still have to end
|
|
49
|
+
* its stream somewhere.
|
|
50
|
+
*/
|
|
51
|
+
export declare function responsesCompletionEvent(descriptor: WinterModelDescriptor | undefined): string;
|
|
52
|
+
export declare class ResponsesStreamMapper {
|
|
53
|
+
private readonly completionEvent;
|
|
54
|
+
/** The event this stream's descriptor says completes a response. Injected so the mapper never reaches for a catalog itself. */
|
|
55
|
+
constructor(completionEvent?: string);
|
|
56
|
+
private sawToolCall;
|
|
57
|
+
private sawRefusal;
|
|
58
|
+
private started;
|
|
59
|
+
/**
|
|
60
|
+
* The completed reasoning items, ordered by the response's OWN `output_index` and, for anything
|
|
61
|
+
* that carried none, by arrival after everything that did.
|
|
62
|
+
*
|
|
63
|
+
* A list rather than a `Map<number, unknown>` keyed on `output_index ?? 0` (minor 6): that default
|
|
64
|
+
* made every indexless item collide on key 0, so a stream carrying two of them replayed ONE — a
|
|
65
|
+
* silently truncated continuation whose next turn fails at the provider, far from here.
|
|
66
|
+
*/
|
|
67
|
+
private readonly reasoningItems;
|
|
68
|
+
private arrivals;
|
|
69
|
+
/** Item ids already reported as unrepresentable, so `added` + `done` for one call is ONE error (minor 5). */
|
|
70
|
+
private readonly reportedUnrepresentable;
|
|
71
|
+
/** item_id -> call_id, so an arguments delta (which carries only the item id) can name its call. */
|
|
72
|
+
private readonly callIdByItem;
|
|
73
|
+
/** call_ids whose arguments arrived as deltas — the final item must not re-send them. */
|
|
74
|
+
private readonly streamedArguments;
|
|
75
|
+
private completed;
|
|
76
|
+
map(data: string): ProviderEvent[];
|
|
77
|
+
/**
|
|
78
|
+
* Called when the byte stream ended.
|
|
79
|
+
*
|
|
80
|
+
* A stream that stopped before `response.completed` is a TRUNCATED turn, and reporting it as a
|
|
81
|
+
* finished one would hand the caller a partial answer as if it were whole. It becomes a
|
|
82
|
+
* non-retryable `network` error: bytes flowed, so R6-6 forbids replaying it.
|
|
83
|
+
*/
|
|
84
|
+
finish(): ProviderEvent[];
|
|
85
|
+
private onCreated;
|
|
86
|
+
private onItemAdded;
|
|
87
|
+
private onArgumentsDelta;
|
|
88
|
+
private onItemDone;
|
|
89
|
+
private onCompleted;
|
|
90
|
+
private noteRefusal;
|
|
91
|
+
/**
|
|
92
|
+
* ONE error per unrepresentable CALL, not one per lifecycle event (minor 5).
|
|
93
|
+
*
|
|
94
|
+
* A call appears twice in the stream (`output_item.added`, then `.done`), so reporting on both
|
|
95
|
+
* emitted two errors for one refusal — which a consumer counting failures reads as two problems.
|
|
96
|
+
*/
|
|
97
|
+
private unrepresentable;
|
|
98
|
+
private unrepresentableError;
|
|
99
|
+
private failureMessage;
|
|
100
|
+
}
|
|
101
|
+
/** What a Responses-speaking adapter (plain OpenAI, codex-oauth, Azure's preview surface) has to supply beyond the request body. */
|
|
102
|
+
export interface ResponsesTurnPlan {
|
|
103
|
+
/** The provider-local model id this plan is for — the descriptor lookup's key (review round 1, C). */
|
|
104
|
+
model: string;
|
|
105
|
+
url: string;
|
|
106
|
+
headers: Record<string, string>;
|
|
107
|
+
endpoint: ResolvedEndpoint;
|
|
108
|
+
ctx: ProviderContext;
|
|
109
|
+
options: OpenAiAdapterOptions;
|
|
110
|
+
body: string;
|
|
111
|
+
beforeAttempt?: (attempt: number) => Promise<void>;
|
|
112
|
+
recover?: (status: number, attempt: number) => Promise<Record<string, string> | undefined>;
|
|
113
|
+
/** Observes the refused response before its body is read — the codex quota manager's only honest source for a limit window (finding I1). */
|
|
114
|
+
onRefused?: (response: Response) => void;
|
|
115
|
+
/** Observed after a successful turn — the codex quota manager's "we are no longer limited" hook. */
|
|
116
|
+
onSuccess?: () => void;
|
|
117
|
+
/** Observed on a rate-limited failure, BEFORE the retry sleeps. The one producer of `rate_limit` events (R6-B). */
|
|
118
|
+
onRateLimited?: (retry: Extract<ProviderEvent, {
|
|
119
|
+
type: "retry";
|
|
120
|
+
}>, queue: EventQueue) => void;
|
|
121
|
+
/** Pre-seeded observations (an `auth_status` from a token refresh that already happened). */
|
|
122
|
+
queue?: EventQueue;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Opens the stream, consumes it, and yields the normalized events.
|
|
126
|
+
*
|
|
127
|
+
* `policy.commit()` fires on the FIRST SSE event, not at header time: WS-13 §13's line is the first
|
|
128
|
+
* response byte consumed, and a 5xx that arrives with headers and an error body is still safely
|
|
129
|
+
* retryable.
|
|
130
|
+
*/
|
|
131
|
+
export declare function streamResponsesTurn(plan: ResponsesTurnPlan, signal: AbortSignal | undefined): AsyncIterable<ProviderEvent>;
|
|
132
|
+
export declare function createResponsesAdapter(options: OpenAiAdapterOptions): ProviderAdapter;
|
|
133
|
+
/** PRIVILEGED headers (R6-L): identifiers that only mean something at the reviewed endpoint they were minted for. `applyPrivilegedHeaders` drops them for a user endpoint. */
|
|
134
|
+
export declare function privilegedHeaders(options: OpenAiAdapterOptions): Record<string, string>;
|
|
135
|
+
/**
|
|
136
|
+
* The plain-OpenAI (and Azure-preview) turn: resolve the endpoint and credential, validate the
|
|
137
|
+
* SELECTION before anything is sent, then stream.
|
|
138
|
+
*
|
|
139
|
+
* Everything that can be refused is refused here, synchronously enough that the fake records ZERO
|
|
140
|
+
* requests — which is what the effort-mapping and limit-rejection fixtures assert on.
|
|
141
|
+
*/
|
|
142
|
+
export declare function responsesTurn(req: TurnRequest, ctx: ProviderContext, options: OpenAiAdapterOptions, fallbackBaseUrl: string | undefined, urlFor: (baseUrl: string) => string, extraProtocolHeaders?: Record<string, string>): AsyncIterable<ProviderEvent>;
|