@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.
@@ -0,0 +1,126 @@
1
+ // Shared wire-shape types for the OpenAI API surface. These mirror the real vendor JSON
2
+ // shapes (not the SDK's internal types — the twin never imports the SDK at runtime; the SDK
3
+ // is exercised only in *.test.ts). Kept minimal but faithful.
4
+
5
+ // ── Chat Completions ────────────────────────────────────────────────────────────────────
6
+ /** A chat message param as the caller sends it (content is a string OR a content-part array). */
7
+ export type ChatMessageParam = {
8
+ role: 'system' | 'user' | 'assistant' | 'tool' | 'developer';
9
+ content?: string | Array<Record<string, unknown>> | null;
10
+ name?: string;
11
+ tool_calls?: ChatToolCall[];
12
+ tool_call_id?: string;
13
+ };
14
+
15
+ /** A function tool_call inside an assistant message (faithful shape). */
16
+ export type ChatToolCall = {
17
+ id: string;
18
+ type: 'function';
19
+ function: { name: string; arguments: string };
20
+ };
21
+
22
+ /** The chat.completion `usage` object — deterministic token counts. */
23
+ export type ChatUsage = {
24
+ prompt_tokens: number;
25
+ completion_tokens: number;
26
+ total_tokens: number;
27
+ completion_tokens_details?: { accepted_prediction_tokens: number; rejected_prediction_tokens: number };
28
+ };
29
+
30
+ /** A per-token logprob entry (faithful `choices[].logprobs.content[]` shape). */
31
+ export type ChatLogprobToken = {
32
+ token: string;
33
+ logprob: number;
34
+ bytes: number[];
35
+ top_logprobs: Array<{ token: string; logprob: number; bytes: number[] }>;
36
+ };
37
+
38
+ /** An audio output object on an assistant message (when `modalities` includes 'audio'). The twin
39
+ * can't synthesize speech, so `data` is a clearly-labeled stub base64 string; the SHAPE
40
+ * (id/data/transcript/expires_at) is vendor-faithful. */
41
+ export type ChatAudioOutput = { id: string; data: string; transcript: string; expires_at: number };
42
+
43
+ export type ChatChoice = {
44
+ index: number;
45
+ message: { role: 'assistant'; content: string | null; tool_calls?: ChatToolCall[]; refusal?: null; audio?: ChatAudioOutput };
46
+ logprobs: { content: ChatLogprobToken[] } | null;
47
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter';
48
+ };
49
+
50
+ /** The unary chat.completion response envelope (faithful shape). */
51
+ export type ChatCompletion = {
52
+ id: string;
53
+ object: 'chat.completion';
54
+ created: number;
55
+ model: string;
56
+ choices: ChatChoice[];
57
+ usage: ChatUsage;
58
+ system_fingerprint: string;
59
+ metadata?: Record<string, unknown> | null;
60
+ };
61
+
62
+ // ── Responses API ───────────────────────────────────────────────────────────────────────
63
+ /** A message output item (the assistant's text turn). */
64
+ export type ResponseMessageItem = {
65
+ type: 'message';
66
+ id: string;
67
+ status: 'completed';
68
+ role: 'assistant';
69
+ content: Array<{ type: 'output_text'; text: string; annotations: unknown[] }>;
70
+ };
71
+ /** A reasoning output item (emitted for reasoning models / when `reasoning.effort` is set). The
72
+ * twin can't run the model, so the reasoning `summary` is a clearly-labeled stub; the item SHAPE
73
+ * (type/id/summary[]) is vendor-faithful. */
74
+ export type ResponseReasoningItem = {
75
+ type: 'reasoning';
76
+ id: string;
77
+ summary: Array<{ type: 'summary_text'; text: string }>;
78
+ };
79
+ export type ResponseOutputItem = ResponseMessageItem | ResponseReasoningItem;
80
+
81
+ /** The Responses API usage object — `output_tokens_details.reasoning_tokens` reports tokens spent
82
+ * on the (stubbed) reasoning item, faithful to the vendor shape. */
83
+ export type ResponseUsage = {
84
+ input_tokens: number;
85
+ output_tokens: number;
86
+ total_tokens: number;
87
+ output_tokens_details?: { reasoning_tokens: number };
88
+ };
89
+
90
+ /** The Responses API response envelope (faithful shape). */
91
+ export type OpenAIResponse = {
92
+ id: string;
93
+ object: 'response';
94
+ created_at: number;
95
+ status: 'completed';
96
+ model: string;
97
+ output: ResponseOutputItem[];
98
+ output_text?: string;
99
+ usage: ResponseUsage;
100
+ reasoning?: { effort: string; summary: string | null };
101
+ previous_response_id?: string;
102
+ };
103
+
104
+ // ── Embeddings ──────────────────────────────────────────────────────────────────────────
105
+ export type Embedding = { object: 'embedding'; index: number; embedding: number[] };
106
+ export type EmbeddingResponse = {
107
+ object: 'list';
108
+ data: Embedding[];
109
+ model: string;
110
+ usage: { prompt_tokens: number; total_tokens: number };
111
+ };
112
+
113
+ // ── Errors ──────────────────────────────────────────────────────────────────────────────
114
+ /** A vendor-shaped error envelope: { error: { message, type, param, code } }. */
115
+ export type OpenAIError = {
116
+ error: { message: string; type: string; param: string | null; code: string | null };
117
+ };
118
+
119
+ // ── Streaming ───────────────────────────────────────────────────────────────────────────
120
+ /** A single Server-Sent Event the streaming path emits (collected, never socketed in tests).
121
+ * `data` is the JSON payload; `[DONE]` is signalled with `done: true` (no data object). */
122
+ export type SseEvent = { data?: Record<string, unknown>; done?: boolean };
123
+
124
+ /** A sink the streaming path writes events into (an injected collector in tests / a real
125
+ * HTTP SSE writer in the server). NO real sockets or setTimeout in the handler. */
126
+ export type SseSink = (event: SseEvent) => void;
@@ -0,0 +1,128 @@
1
+ // OpenAI webhooks — REAL HMAC-signed delivery, Standard-Webhooks scheme.
2
+ //
3
+ // Real OpenAI delivers asynchronous job-completion notifications (batch / fine-tuning /
4
+ // background responses / evals) to a registered endpoint, signed with the Standard Webhooks
5
+ // scheme (the same scheme svix popularized): the signing secret is `whsec_<base64>`, the HMAC
6
+ // key is the base64-decoded portion, and the delivery carries `webhook-id`, `webhook-timestamp`,
7
+ // and `webhook-signature` headers where the signature is `v1,<base64 HMAC-SHA256(key,
8
+ // `${id}.${timestamp}.${payload}`)>`. The SDK verifies with `client.webhooks.unwrap()`. We
9
+ // reproduce the scheme BYTE-FOR-BYTE so a real consumer (or the SDK's unwrap) verifies twin-
10
+ // delivered webhooks unchanged — the signature is REAL, not a stub.
11
+ //
12
+ // `node:crypto` is required LAZILY (server-only) so this module is browser-bundle safe.
13
+ type NodeCrypto = typeof import('node:crypto');
14
+ function nodeCrypto(): NodeCrypto {
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ return require('node:crypto') as NodeCrypto;
17
+ }
18
+
19
+ /** The thin webhook envelope OpenAI POSTs: { object:'event', id, type, created_at, data:{id} }. */
20
+ export type OpenAIWebhookEvent = {
21
+ object: 'event';
22
+ id: string;
23
+ type: string;
24
+ created_at: number;
25
+ data: { id: string; [k: string]: unknown };
26
+ };
27
+
28
+ /** Decode a `whsec_<base64>` signing secret to its raw HMAC key bytes. */
29
+ function secretKeyBytes(secret: string): Buffer {
30
+ const b64 = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret;
31
+ return Buffer.from(b64, 'base64');
32
+ }
33
+
34
+ /**
35
+ * Compute the `webhook-signature` header value for a delivery: `v1,<base64 sig>` over
36
+ * `${id}.${timestamp}.${payload}` keyed by the (base64-decoded) signing secret — the Standard
37
+ * Webhooks scheme. A real Standard-Webhooks / svix verifier accepts this unchanged.
38
+ */
39
+ export function computeOpenAIWebhookSignature(id: string, timestamp: number, payload: string, secret: string): string {
40
+ const key = secretKeyBytes(secret);
41
+ const signed = `${id}.${timestamp}.${payload}`;
42
+ const sig = nodeCrypto().createHmac('sha256', key).update(signed, 'utf8').digest('base64');
43
+ return `v1,${sig}`;
44
+ }
45
+
46
+ export class OpenAIWebhookVerificationError extends Error {
47
+ constructor(message: string) {
48
+ super(message);
49
+ this.name = 'OpenAIWebhookVerificationError';
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Verify a webhook payload + Standard-Webhooks headers against the signing secret and return the
55
+ * parsed event (mirrors the SDK's `webhooks.unwrap`). Throws on a missing/malformed header, a
56
+ * signature mismatch, or (when `tolerance` is given) a stale timestamp.
57
+ */
58
+ export function verifyOpenAIWebhook(payload: string, headers: Record<string, string>, secret: string, opts: { tolerance?: number; now?: number } = {}): OpenAIWebhookEvent {
59
+ const lower: Record<string, string> = {};
60
+ for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
61
+ const id = lower['webhook-id'];
62
+ const ts = Number(lower['webhook-timestamp']);
63
+ const sigHeader = lower['webhook-signature'];
64
+ if (!id || !sigHeader || !Number.isFinite(ts)) throw new OpenAIWebhookVerificationError('Missing required webhook headers (webhook-id / webhook-timestamp / webhook-signature).');
65
+ if (opts.tolerance !== undefined) {
66
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
67
+ if (Math.abs(now - ts) > opts.tolerance) throw new OpenAIWebhookVerificationError('Webhook timestamp outside tolerance.');
68
+ }
69
+ const expected = computeOpenAIWebhookSignature(id, ts, payload, secret).slice('v1,'.length);
70
+ const expectedBuf = Buffer.from(expected, 'utf8');
71
+ // The header may carry multiple space-separated `v<n>,<sig>` pairs.
72
+ const matched = sigHeader.split(' ').some((part) => {
73
+ const comma = part.indexOf(',');
74
+ if (comma === -1) return false;
75
+ const sig = part.slice(comma + 1);
76
+ const sigBuf = Buffer.from(sig, 'utf8');
77
+ return sigBuf.length === expectedBuf.length && nodeCrypto().timingSafeEqual(sigBuf, expectedBuf);
78
+ });
79
+ if (!matched) throw new OpenAIWebhookVerificationError('No matching signature found.');
80
+ return JSON.parse(payload) as OpenAIWebhookEvent;
81
+ }
82
+
83
+ /** The webhook event types OpenAI emits (a faithful slice of the published set). */
84
+ export const OPENAI_WEBHOOK_EVENT_TYPES = [
85
+ 'response.completed',
86
+ 'response.cancelled',
87
+ 'response.failed',
88
+ 'response.incomplete',
89
+ 'batch.completed',
90
+ 'batch.cancelled',
91
+ 'batch.expired',
92
+ 'batch.failed',
93
+ 'fine_tuning.job.succeeded',
94
+ 'fine_tuning.job.cancelled',
95
+ 'fine_tuning.job.failed',
96
+ 'eval.run.succeeded',
97
+ 'eval.run.canceled',
98
+ 'eval.run.failed',
99
+ ] as const;
100
+
101
+ /**
102
+ * Build a SIGNED webhook delivery for an async job completion: the thin event envelope + the
103
+ * Standard-Webhooks headers carrying a REAL HMAC signature. Deterministic + offline.
104
+ */
105
+ export function buildSignedOpenAIWebhook(args: {
106
+ type: string;
107
+ resourceId: string;
108
+ secret: string;
109
+ occurredAt: string;
110
+ webhookId: string;
111
+ extra?: Record<string, unknown>;
112
+ }): { headers: Record<string, string>; body: string; event: OpenAIWebhookEvent } {
113
+ const ts = Math.floor(Date.parse(args.occurredAt) / 1000);
114
+ const event: OpenAIWebhookEvent = {
115
+ object: 'event',
116
+ id: args.webhookId,
117
+ type: args.type,
118
+ created_at: ts,
119
+ data: { id: args.resourceId, ...(args.extra ?? {}) },
120
+ };
121
+ const body = JSON.stringify(event);
122
+ const headers = {
123
+ 'webhook-id': args.webhookId,
124
+ 'webhook-timestamp': String(ts),
125
+ 'webhook-signature': computeOpenAIWebhookSignature(args.webhookId, ts, body, args.secret),
126
+ };
127
+ return { headers, body, event };
128
+ }
@@ -0,0 +1,19 @@
1
+ # openai-openapi-operations.json — provenance
2
+
3
+ The operation-inventory denominator for `../openai-spec-census.json`: a scope-filtered subset of
4
+ the vendor's published spec, enumerated by `deriveFromOpenAPI` (`packages/twin/tooling/src/derive.ts`).
5
+
6
+ - **Upstream:** https://github.com/openai/openai-openapi — `openapi.yaml`, fetched 2026-09-02 from
7
+ `https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml` at commit
8
+ `18a43ed13461a01fce5b9cc93c86843c550734a3` (2026-09-01), spec `info.version` 2.3.0.
9
+ - **How it was built:** `bun scripts/spec-census-bootstrap.ts --vendor openai --from-file <openapi.yaml> --scope-paths /assistants,/audio,/batches,/chat/,/containers,/embeddings,/evals,/files,/fine_tuning,/images,/models,/moderations,/organization,/responses,/threads,/uploads,/vector_stores --apply`
10
+ over a copy of the upstream file fetched 2026-09-02; operation metadata only (operationId, summary, tags,
11
+ first response), schemas dropped. The gate never fetches — refreshing the fixture is
12
+ `bun scripts/spec-refresh.ts --vendor openai --from-file <newer spec> --apply` over a newly fetched copy.
13
+ - **Scope (222 of 281 upstream operations):** every operation whose path starts with one of
14
+ `/assistants`, `/audio`, `/batches`, `/chat/`, `/containers`, `/embeddings`, `/evals`, `/files`, `/fine_tuning`, `/images`, `/models`, `/moderations`, `/organization`, `/responses`, `/threads`, `/uploads`, `/vector_stores`.
15
+ - **Mapping:** 128 operations mapped to capability ids by segment match at birth, then reviewed by
16
+ hand against the router (`routeOpenAI` in `../src/openai-twin.ts`) and each capability's `verify()`:
17
+ 60 entries were re-ruled, landing at **97 capability / 0 outOfScope / 125 unmapped** (the ratchet
18
+ baseline). Every later ruling (an `outOfScope` reason, a remap) is an edit to the census, never to
19
+ this fixture.