@volter/twin-openai 0.1.0
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 +202 -0
- package/README.md +168 -0
- package/package.json +62 -0
- package/src/cli.ts +26 -0
- package/src/index.ts +108 -0
- package/src/openai-budget.ts +173 -0
- package/src/openai-capabilities.ts +1556 -0
- package/src/openai-conformance.ts +112 -0
- package/src/openai-connector.ts +337 -0
- package/src/openai-models.ts +36 -0
- package/src/openai-scenario.ts +136 -0
- package/src/openai-server.ts +167 -0
- package/src/openai-stub.ts +247 -0
- package/src/openai-twin.ts +2100 -0
- package/src/openai-types.ts +126 -0
- package/src/openai-webhooks.ts +128 -0
- package/test-fixtures/openai-openapi-operations.SOURCE.md +19 -0
- package/test-fixtures/openai-openapi-operations.json +2959 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// OpenAI's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings `liveOpenAIExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
|
|
3
|
+
// window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
|
|
4
|
+
// lives ONCE in the vendor-agnostic kernel (`@volter/twin` → `rateBudget.ts`). Read that module's
|
|
5
|
+
// header for the full rationale AND for the honest list of what the guard does not guarantee (an
|
|
6
|
+
// injected clock or ledger path still defeats it — it guards carelessness, not malice).
|
|
7
|
+
//
|
|
8
|
+
// ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// A real ~4.5-DAY vendor lockout (Figma, 2026-07-25) happened because raw API calls were made
|
|
10
|
+
// outside the pack's connector — no cache, no batching, no ceiling. Discipline only binds the code
|
|
11
|
+
// that follows it; a BUDGET binds the code that does not.
|
|
12
|
+
//
|
|
13
|
+
// ── HOW THE CEILING WAS CHOSEN: NO SCALAR IS PUBLISHED ──────────────────────────────────────
|
|
14
|
+
// Unlike Notion or Cal.com, OpenAI publishes NO scalar per-key request limit. From
|
|
15
|
+
// https://developers.openai.com/api/docs/guides/rate-limits (read 2026-07-26): limits are per
|
|
16
|
+
// ORGANIZATION and PROJECT, expressed in RPM/RPD/TPM/TPD/IPM, and they differ PER MODEL and per
|
|
17
|
+
// tier — the actual figures live on the account's own limits page, not in the docs. The docs carry
|
|
18
|
+
// exactly one scalar this pack could be bound by: Vector Store ingestion is 300 requests per minute
|
|
19
|
+
// per vector store ID. The Batch API's limit is a queued-input-TOKEN quota, not an RPM.
|
|
20
|
+
//
|
|
21
|
+
// So this budget deliberately does NOT model the vendor's limit, and the numbers below are NOT
|
|
22
|
+
// derived from one. Because there is no documented figure to justify going higher, the ceiling is
|
|
23
|
+
// pinned at the kernel's own undeclared fallback in every dimension: 60 weighted units per 60s at
|
|
24
|
+
// `defaultWeight` 2 — 30 calls a minute, exactly `DEFAULT_RATE_BUDGET`, with no endpoint priced
|
|
25
|
+
// CHEAPER than the fallback would price it. What this declaration adds over the fallback is
|
|
26
|
+
// therefore not headroom, it is RESOLUTION: the endpoints that cost real money get priced up.
|
|
27
|
+
//
|
|
28
|
+
// It bounds the 60s AVERAGE; it does not pace (the kernel refuses, it never sleeps). The backstop
|
|
29
|
+
// for a sub-second burst is the cooldown: OpenAI's `x-ratelimit-remaining-requests: 0` and its
|
|
30
|
+
// `retry-after` are both read off the response and turn into a persisted refusal.
|
|
31
|
+
//
|
|
32
|
+
// ── HOW THE WEIGHTS WERE CHOSEN (and what is a judgement call) ───────────────────────────────
|
|
33
|
+
// OpenAI counts requests per endpoint family, so a flat price is the faithful default. Two
|
|
34
|
+
// deliberate exceptions, both judgement calls rather than published costs:
|
|
35
|
+
// • The token-metered inference paths (`/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`,
|
|
36
|
+
// `/v1/images/*`, `/v1/audio/*`) cost 5. There TPM — not RPM — is the binding limit, so one
|
|
37
|
+
// request is worth far more of the org's real allowance than a file-list poll. The connector
|
|
38
|
+
// never calls them, but the executor is generic and a caller could; an unclassified expensive
|
|
39
|
+
// endpoint is exactly the shape that caused the lockout.
|
|
40
|
+
// • Vector-store FILE ingestion costs 4, because it is the one endpoint with a documented
|
|
41
|
+
// per-minute scalar (300/min per store) AND the recursive fan-out shape — one store ingest is a
|
|
42
|
+
// loop over every file. At weight 4 at most 15 land in a window, a twentieth of that 300.
|
|
43
|
+
import {
|
|
44
|
+
declareRateBudget,
|
|
45
|
+
rateBudgetPath,
|
|
46
|
+
rateBudgetWeight,
|
|
47
|
+
RateBudget,
|
|
48
|
+
type RateBudgetDeclaration,
|
|
49
|
+
type RateBudgetOptions,
|
|
50
|
+
type RateBudgetReservation,
|
|
51
|
+
type RateBudgetSnapshot,
|
|
52
|
+
} from '@volter/twin';
|
|
53
|
+
|
|
54
|
+
const VENDOR = 'openai';
|
|
55
|
+
|
|
56
|
+
/** Rolling window, in ms. Spend older than this is pruned. */
|
|
57
|
+
export const OPENAI_BUDGET_WINDOW_MS = 60_000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Weighted units allowed inside one window. 60/60s at `defaultWeight` 2 = 30 calls a minute —
|
|
61
|
+
* EXACTLY the kernel's undeclared fallback, because OpenAI publishes no scalar that would justify
|
|
62
|
+
* more. See the header.
|
|
63
|
+
*/
|
|
64
|
+
export const OPENAI_BUDGET_CEILING = 60;
|
|
65
|
+
|
|
66
|
+
/** Seconds. A `retry-after` above this means the key is throttled hard — fail loudly, don't sleep. */
|
|
67
|
+
export const OPENAI_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
68
|
+
|
|
69
|
+
/** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs. judged. */
|
|
70
|
+
export const OPENAI_CALL_WEIGHTS = {
|
|
71
|
+
/** `/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/images/*`, `/v1/audio/*` —
|
|
72
|
+
* token/compute-metered, where TPM rather than RPM is the binding limit. */
|
|
73
|
+
inference: 5,
|
|
74
|
+
/** `POST /v1/vector_stores/:id/files` — documented 300 RPM per store, and a per-file fan-out. */
|
|
75
|
+
ingest: 4,
|
|
76
|
+
/** Everything else: file/batch/fine-tune/vector-store list, retrieve, create, cancel, delete. */
|
|
77
|
+
other: 2,
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/** THE PACK'S DECLARATION — pure data, the only OpenAI-specific thing in the whole budget. */
|
|
81
|
+
export const OPENAI_RATE_BUDGET: RateBudgetDeclaration = {
|
|
82
|
+
windowMs: OPENAI_BUDGET_WINDOW_MS,
|
|
83
|
+
ceiling: OPENAI_BUDGET_CEILING,
|
|
84
|
+
defaultWeight: OPENAI_CALL_WEIGHTS.other,
|
|
85
|
+
maxRetryAfterSeconds: OPENAI_BUDGET_MAX_RETRY_AFTER_S,
|
|
86
|
+
rules: [
|
|
87
|
+
{ match: '^POST /v1/(chat/completions|responses|embeddings|moderations)$', weight: OPENAI_CALL_WEIGHTS.inference },
|
|
88
|
+
{ match: '^POST /v1/(images|audio|realtime|videos)/', weight: OPENAI_CALL_WEIGHTS.inference },
|
|
89
|
+
{ match: '^POST /v1/vector_stores/[^/]+/files', weight: OPENAI_CALL_WEIGHTS.ingest },
|
|
90
|
+
],
|
|
91
|
+
reason:
|
|
92
|
+
'OpenAI publishes NO scalar per-key request limit (developers.openai.com/api/docs/guides/' +
|
|
93
|
+
'rate-limits, read 2026-07-26): limits are per organization and project, in RPM/RPD/TPM/TPD/IPM, ' +
|
|
94
|
+
"differ per model and tier, and the figures live on the account's own limits page. The one " +
|
|
95
|
+
'documented scalar that can bind this pack is Vector Store ingestion at 300 requests/minute per ' +
|
|
96
|
+
'store. Because no published figure justifies going higher, the ceiling is pinned at the kernel ' +
|
|
97
|
+
"fallback in EVERY dimension — 60 units / 60s at defaultWeight 2 = 30 calls/min, and no endpoint " +
|
|
98
|
+
'is priced cheaper than the fallback would price it; the declaration buys resolution, not ' +
|
|
99
|
+
'headroom. Inference paths cost 5 (TPM, not RPM, binds them) and vector-store file ingestion ' +
|
|
100
|
+
'costs 4 (the documented 300/min scalar, and a per-file fan-out) — judgement calls, not published ' +
|
|
101
|
+
'costs. The window bounds the 60s AVERAGE and does not pace; the 429/`x-ratelimit-remaining-*: 0` ' +
|
|
102
|
+
'cooldown is the backstop for a sub-second burst.',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Declared at module load, so merely importing this module (which `openai-connector.ts` does) is
|
|
106
|
+
// enough to arm the real ceiling. `RateBudget` reads its policy live precisely so this declaration
|
|
107
|
+
// takes effect the moment it lands, and constructing through the subclass below (which imports this
|
|
108
|
+
// module) is what makes the ordering a non-issue in practice.
|
|
109
|
+
declareRateBudget(VENDOR, OPENAI_RATE_BUDGET);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Price one call. The key is `"<METHOD> <path>"` with the query string split off, so a rule can
|
|
113
|
+
* price by method (a write is not a read) without the kernel knowing anything about OpenAI. An
|
|
114
|
+
* unclassified endpoint still costs `defaultWeight` — nothing is ever free.
|
|
115
|
+
*/
|
|
116
|
+
export function openaiCallWeight(method: string, path: string): number {
|
|
117
|
+
const { bare, query } = splitQuery(path);
|
|
118
|
+
// UPPER-CASE the method: `fetch` normalizes a known lowercase method before sending, so
|
|
119
|
+
// `execute('post', …)` really does issue a POST and must be priced as one.
|
|
120
|
+
return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `/v1/x?a=1` -> `{ bare: '/v1/x', query: { a: '1' } }`. Rules match the path; `whenQuery*` the query.
|
|
125
|
+
*
|
|
126
|
+
* NORMALIZED, because the anchored rules are otherwise trivially evaded (§9 finding, 2026-07-26):
|
|
127
|
+
* `fetch` upper-cases a known method before sending, so `execute('post', …)` issues a real WRITE
|
|
128
|
+
* that a `^POST ` rule would price as a read; and a trailing slash makes a path miss a `$` anchor
|
|
129
|
+
* while most routers treat it as the same endpoint. Both are input variations, not attacks, and
|
|
130
|
+
* either one silently voids the "expensive endpoints are priced up" claim the ceiling rests on.
|
|
131
|
+
*/
|
|
132
|
+
function splitQuery(path: string): { bare: string; query: Record<string, string> } {
|
|
133
|
+
const at = path.indexOf('?');
|
|
134
|
+
const query: Record<string, string> = {};
|
|
135
|
+
if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
|
|
136
|
+
const raw = at === -1 ? path : path.slice(0, at);
|
|
137
|
+
// Collapse a trailing slash, but never turn the root path into the empty string.
|
|
138
|
+
const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
|
|
139
|
+
return { bare, query };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Where OpenAI's ledger lives. Token-keyed and cwd-independent by default (the limit is per
|
|
143
|
+
* organization/project, i.e. per key, so a cwd-scoped ledger would hand the same key a fresh
|
|
144
|
+
* allowance in every checkout, worktree and CI matrix leg); pass `root` for world-scoped accounting. */
|
|
145
|
+
export function openaiBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
|
|
146
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
147
|
+
// VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
|
|
148
|
+
// excess-property check only catches object literals) must not redirect this pack's ledger to
|
|
149
|
+
// another vendor's file.
|
|
150
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Construction options for OpenAI's budget. The vendor is fixed; everything else may only TIGHTEN. */
|
|
154
|
+
export type OpenAIBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* OpenAI's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
|
|
158
|
+
* not an alias, so `budget instanceof OpenAIBudget` in `liveOpenAIExecute` means "a budget that
|
|
159
|
+
* accounts against OPENAI's ledger under OPENAI's ceiling": another vendor's `RateBudget` (with its
|
|
160
|
+
* own, possibly larger, ceiling) is NOT assignable there.
|
|
161
|
+
*/
|
|
162
|
+
export class OpenAIBudget extends RateBudget {
|
|
163
|
+
constructor(opts: OpenAIBudgetOptions = {}) {
|
|
164
|
+
super({ ...opts, vendor: VENDOR });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
|
|
169
|
+
* which one refused, and `err.kind` says why. */
|
|
170
|
+
export { RateBudgetError as OpenAIBudgetError } from '@volter/twin';
|
|
171
|
+
export type { RateBudgetErrorKind as OpenAIBudgetErrorKind } from '@volter/twin';
|
|
172
|
+
export type OpenAIBudgetReservation = RateBudgetReservation;
|
|
173
|
+
export type OpenAIBudgetSnapshot = RateBudgetSnapshot;
|