@volter/twin-openai 0.1.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +33 -30
  2. package/defaults/handlers.json +10 -0
  3. package/dist/defaults/handlers.json +10 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +29 -0
  6. package/dist/src/generated/surface.gen.json +1 -0
  7. package/dist/src/generated/ui.gen.json +1 -0
  8. package/dist/src/index.d.ts +19 -0
  9. package/dist/src/index.js +72 -0
  10. package/dist/src/manifest.d.ts +6 -0
  11. package/dist/src/manifest.js +323 -0
  12. package/dist/src/openai-budget.d.ts +53 -0
  13. package/dist/src/openai-budget.js +147 -0
  14. package/dist/src/openai-capabilities.d.ts +4 -0
  15. package/dist/src/openai-capabilities.js +1569 -0
  16. package/dist/src/openai-conformance.d.ts +13 -0
  17. package/dist/src/openai-conformance.js +116 -0
  18. package/dist/src/openai-connector.d.ts +86 -0
  19. package/dist/src/openai-connector.js +291 -0
  20. package/dist/src/openai-media.d.ts +43 -0
  21. package/dist/src/openai-media.js +257 -0
  22. package/dist/src/openai-models.d.ts +74 -0
  23. package/dist/src/openai-models.js +148 -0
  24. package/dist/src/openai-scenario.d.ts +51 -0
  25. package/dist/src/openai-scenario.js +166 -0
  26. package/dist/src/openai-server.d.ts +40 -0
  27. package/dist/src/openai-server.js +126 -0
  28. package/dist/src/openai-stub.d.ts +82 -0
  29. package/dist/src/openai-stub.js +256 -0
  30. package/dist/src/openai-twin.d.ts +182 -0
  31. package/dist/src/openai-twin.js +1117 -0
  32. package/dist/src/openai-types.d.ts +194 -0
  33. package/dist/src/openai-types.js +4 -0
  34. package/dist/src/openai-webhooks.d.ts +47 -0
  35. package/dist/src/openai-webhooks.js +99 -0
  36. package/dist/src/screens/api-keys.d.ts +16 -0
  37. package/dist/src/screens/api-keys.js +131 -0
  38. package/dist/src/screens/session.d.ts +22 -0
  39. package/dist/src/screens/session.js +115 -0
  40. package/dist/src/semantics/assistants.d.ts +2 -0
  41. package/dist/src/semantics/assistants.js +331 -0
  42. package/dist/src/semantics/audio.d.ts +2 -0
  43. package/dist/src/semantics/audio.js +27 -0
  44. package/dist/src/semantics/batches.d.ts +4 -0
  45. package/dist/src/semantics/batches.js +86 -0
  46. package/dist/src/semantics/chat-completions.d.ts +3 -0
  47. package/dist/src/semantics/chat-completions.js +58 -0
  48. package/dist/src/semantics/containers.d.ts +2 -0
  49. package/dist/src/semantics/containers.js +147 -0
  50. package/dist/src/semantics/embeddings.d.ts +2 -0
  51. package/dist/src/semantics/embeddings.js +13 -0
  52. package/dist/src/semantics/evals.d.ts +2 -0
  53. package/dist/src/semantics/evals.js +173 -0
  54. package/dist/src/semantics/files.d.ts +13 -0
  55. package/dist/src/semantics/files.js +59 -0
  56. package/dist/src/semantics/fine-tuning.d.ts +4 -0
  57. package/dist/src/semantics/fine-tuning.js +178 -0
  58. package/dist/src/semantics/images.d.ts +2 -0
  59. package/dist/src/semantics/images.js +18 -0
  60. package/dist/src/semantics/index.d.ts +8 -0
  61. package/dist/src/semantics/index.js +46 -0
  62. package/dist/src/semantics/models.d.ts +2 -0
  63. package/dist/src/semantics/models.js +34 -0
  64. package/dist/src/semantics/moderations.d.ts +2 -0
  65. package/dist/src/semantics/moderations.js +12 -0
  66. package/dist/src/semantics/organization.d.ts +2 -0
  67. package/dist/src/semantics/organization.js +67 -0
  68. package/dist/src/semantics/progress.d.ts +22 -0
  69. package/dist/src/semantics/progress.js +63 -0
  70. package/dist/src/semantics/responses.d.ts +3 -0
  71. package/dist/src/semantics/responses.js +153 -0
  72. package/dist/src/semantics/shared.d.ts +32 -0
  73. package/dist/src/semantics/shared.js +69 -0
  74. package/dist/src/semantics/uploads.d.ts +2 -0
  75. package/dist/src/semantics/uploads.js +84 -0
  76. package/dist/src/semantics/vector-stores.d.ts +2 -0
  77. package/dist/src/semantics/vector-stores.js +281 -0
  78. package/dist/test-fixtures/openai-openapi-operations.SOURCE.md +18 -0
  79. package/dist/test-fixtures/openai-openapi-operations.json +1849 -0
  80. package/package.json +21 -10
  81. package/src/cli.ts +9 -7
  82. package/src/generated/surface.gen.json +1 -0
  83. package/src/generated/ui.gen.json +1 -0
  84. package/src/index.ts +20 -10
  85. package/src/manifest.ts +343 -0
  86. package/src/openai-budget.ts +4 -4
  87. package/src/openai-capabilities.ts +177 -195
  88. package/src/openai-conformance.ts +1 -1
  89. package/src/openai-connector.ts +40 -43
  90. package/src/openai-media.ts +225 -0
  91. package/src/openai-models.ts +145 -15
  92. package/src/openai-scenario.ts +46 -10
  93. package/src/openai-server.ts +65 -108
  94. package/src/openai-stub.ts +54 -30
  95. package/src/openai-twin.ts +760 -1665
  96. package/src/openai-types.ts +24 -6
  97. package/src/openai-webhooks.ts +3 -2
  98. package/src/screens/api-keys.tsx +138 -0
  99. package/src/screens/session.tsx +131 -0
  100. package/src/semantics/assistants.ts +336 -0
  101. package/src/semantics/audio.ts +31 -0
  102. package/src/semantics/batches.ts +88 -0
  103. package/src/semantics/chat-completions.ts +66 -0
  104. package/src/semantics/containers.ts +151 -0
  105. package/src/semantics/embeddings.ts +19 -0
  106. package/src/semantics/evals.ts +182 -0
  107. package/src/semantics/files.ts +67 -0
  108. package/src/semantics/fine-tuning.ts +185 -0
  109. package/src/semantics/images.ts +23 -0
  110. package/src/semantics/index.ts +52 -0
  111. package/src/semantics/models.ts +41 -0
  112. package/src/semantics/moderations.ts +14 -0
  113. package/src/semantics/organization.ts +76 -0
  114. package/src/semantics/progress.ts +72 -0
  115. package/src/semantics/responses.ts +151 -0
  116. package/src/semantics/shared.ts +82 -0
  117. package/src/semantics/uploads.ts +92 -0
  118. package/src/semantics/vector-stores.ts +279 -0
  119. package/test-fixtures/openai-openapi-operations.SOURCE.md +4 -5
  120. package/test-fixtures/openai-openapi-operations.json +224 -1334
@@ -1,38 +1,28 @@
1
- // OpenAI twin REQUEST HANDLER — the canonical OpenAI API surface for the twin.
2
- // Contract: handleOpenAITwinRequest({method, path, body}) -> {status, body}. It is the
3
- // faithful OpenAI API the real `openai` SDK (pointed at this baseURL) talks to UNMODIFIED.
4
- //
5
- // THE HONEST DESIGN: the twin cannot run the model, so `POST /v1/chat/completions` and
6
- // `POST /v1/responses` return a DETERMINISTIC STUB completion (openai-stub.ts) clearly labeled
7
- // a twin stub — it NEVER pretends to be real model output. `POST /v1/embeddings` returns
8
- // DETERMINISTIC pseudo-vectors (never real embedding values). But the ENTIRE PROTOCOL ENVELOPE
9
- // is vendor-faithful: response shapes, streaming SSE chunks, tool_calls, finish_reason,
10
- // deterministic usage. The genuinely stateful + static surface is real, not stubbed:
11
- // • GET /v1/models (+ /:id) — static catalog (openai-models.ts)
12
- // • POST/GET/DELETE /v1/files — stateful (kernel action log)
13
- // • POST/GET /v1/batches (+ cancel) — stateful
14
- // • POST /v1/moderations — deterministic classifier
15
- // • Fine-tuning jobs (create/get/list/cancel/events) — stateful
16
- // • Vector stores (+ files) — stateful
17
- // • POST /v1/images/generations — response shape with a placeholder URL (real pixels
18
- // are out of scope)
19
- //
20
- // State lives in the kernel action log (D1): all writes are local actions, reads are the
21
- // projection. No real OpenAI is ever called from this path (D4). Streaming uses an INJECTED
22
- // sink — no real sockets / setTimeout (D5 verify is offline + deterministic).
23
- import { applyTwinWrite, projectResources } from '@volter/twin';
24
- import { OPENAI_MODELS, findModel } from './openai-models.ts';
1
+ // OpenAI twin: the pack's behaviour and its in-process door. `handleOpenAITwinRequest` answers one
2
+ // request through the pack's fetch (openai-server.ts), which the derived dispatch owns: an operation
3
+ // OpenAI's spec publishes is a semantics handler (semantics/), the derived core (manifest.ts), or
4
+ // OpenAI's unknown-URL 404. What lives here is what those share:
5
+ // • the generative stubs: chat completions and responses (validation, the deterministic stub or a
6
+ // scripted scenario's turn, the streaming event sequences), embeddings (pseudo-vectors),
7
+ // moderations, images and audio (labeled placeholders); never a model's output;
8
+ // • the Responses API's atomic id allocation and background-poll completion;
9
+ // • the usage and costs reports over the recorded usage.
10
+ // No real OpenAI is called from this path.
11
+ import { applyTwinWriteAtomic, type ScenarioDecision } from '@volter/world-core';
25
12
  import {
26
13
  buildLogprobs,
27
14
  countPromptTokens,
28
15
  estimateTokens,
16
+ lastUserText,
29
17
  moderateText,
30
18
  pseudoEmbedding,
31
19
  stubAssistantText,
32
20
  stubJsonObject,
33
21
  stubToolCall,
34
22
  } from './openai-stub.ts';
35
- import { type OpenAIScenarioEngine, type OpenAIScenarioRespond, realizeOpenAIRespond, type ScriptedResult } from './openai-scenario.ts';
23
+ import { DEFAULT_EFFORT, modelOf, reasons, unsupported } from './openai-models.ts';
24
+ import { type OpenAIScenarioEngine, scenarioTurn, scriptedChoice } from './openai-scenario.ts';
25
+ import { audioFormat, base64, imageFormat, partBytes, placeholderPng, pngSize, requestedSize, silentAac, silentFlac, silentMp3, silentOpus, silentPcm, silentWav, wavSeconds } from './openai-media.ts';
36
26
  import type {
37
27
  ChatChoice,
38
28
  ChatCompletion,
@@ -44,11 +34,14 @@ import type {
44
34
  OpenAIResponse,
45
35
  ResponseMessageItem,
46
36
  ResponseOutputItem,
37
+ ResponseFunctionCallItem,
47
38
  ResponseReasoningItem,
48
39
  ResponseUsage,
49
40
  SseSink,
50
41
  } from './openai-types.ts';
51
42
 
43
+ import { createOpenAITwinFetch } from './openai-server.ts';
44
+
52
45
  const SERVICE = 'openai';
53
46
 
54
47
  export type OpenAIRequest = {
@@ -56,7 +49,8 @@ export type OpenAIRequest = {
56
49
  scenarioEngine?: OpenAIScenarioEngine;
57
50
  method: string;
58
51
  path: string;
59
- body?: string;
52
+ /** JSON text, or a multipart form (an upload) */
53
+ body?: string | FormData;
60
54
  occurredAt?: string;
61
55
  root?: string;
62
56
  readOnly?: boolean;
@@ -66,8 +60,8 @@ export type OpenAIRequest = {
66
60
  * (capability verify, connector) omit BOTH and are not auth-gated — the twin can't validate
67
61
  * against real keys, so the modeled failure is the CHECKABLE missing/sentinel-invalid case. */
68
62
  apiKey?: string;
69
- /** Lower-cased request headers (e.g. `authorization`, `idempotency-key`) the HTTP server passes
70
- * through so the handler can model auth (401), the rate-limit trigger (429), and idempotency. */
63
+ /** Lower-cased request headers (e.g. `authorization`) the HTTP server passes through so the
64
+ * handler can model auth (401) and the rate-limit trigger (429). */
71
65
  headers?: Record<string, string>;
72
66
  /** When set on a streaming POST, chunks are written here (no sockets). */
73
67
  sseSink?: SseSink;
@@ -83,116 +77,10 @@ function errBody(type: string, message: string, code: string | null = null, para
83
77
  function invalidRequest(message: string, param: string | null = null, code: string | null = null): OpenAIResponseEnvelope {
84
78
  return { status: 400, body: errBody('invalid_request_error', message, code, param) };
85
79
  }
86
- function notFound(message: string, code: string | null = null): OpenAIResponseEnvelope {
87
- return { status: 404, body: errBody('invalid_request_error', message, code) };
88
- }
89
- function authError(message: string, code = 'invalid_api_key'): OpenAIResponseEnvelope {
90
- // Real OpenAI 401s carry error.type:'invalid_request_error' with a code like 'invalid_api_key'.
91
- return { status: 401, body: errBody('invalid_request_error', message, code) };
92
- }
93
-
94
- // ── modeled authentication (401) ────────────────────────────────────────────────────────
95
- // Real OpenAI requires a bearer credential on every request and returns 401 when it is missing
96
- // or invalid. The twin can't validate against real keys, so it models the CHECKABLE failures: a
97
- // missing credential, and a reserved sentinel ('sk-invalid'/'invalid') for the invalid-key path.
98
- // Any other non-empty key is accepted. Trusted in-process calls (verify/connector) carry NEITHER
99
- // `headers` nor `apiKey` and are NOT auth-gated; the real `openai` SDK always sends a key → passes.
100
- function checkAuth(req: OpenAIRequest): OpenAIResponseEnvelope | null {
101
- const auth = req.headers?.['authorization'];
102
- const bearer = typeof auth === 'string' && auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : '';
103
- const key = (req.apiKey ?? '').trim() || bearer;
104
- if (!key) return authError('You didn\'t provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY).', 'invalid_api_key');
105
- if (key === 'sk-invalid' || key === 'invalid') return authError('Incorrect API key provided. You can find your API key at https://platform.openai.com/account/api-keys.', 'invalid_api_key');
106
- return null;
107
- }
108
-
109
- // ── modeled rate limiting (429) ─────────────────────────────────────────────────────────
110
- // Rate limits are non-deterministic in production, so the twin exposes a DETERMINISTIC opt-in
111
- // trigger: a request carrying `x-twin-force-rate-limit: 1` (or `true`) returns the faithful 429
112
- // envelope (error.type:'rate_limit_exceeded') + Retry-After and the x-ratelimit-* header family.
113
- // (No real timing/quotas — the twin can't reproduce them; this is the checkable plumbing.)
114
- function rateLimitError(): OpenAIResponseEnvelope {
115
- return {
116
- status: 429,
117
- body: errBody('rate_limit_exceeded', 'Rate limit reached for requests. Limit your request rate or retry after the indicated delay.', 'rate_limit_exceeded'),
118
- headers: {
119
- 'retry-after': '1',
120
- 'x-ratelimit-limit-requests': '10000',
121
- 'x-ratelimit-remaining-requests': '0',
122
- 'x-ratelimit-reset-requests': '1s',
123
- 'x-ratelimit-limit-tokens': '2000000',
124
- 'x-ratelimit-remaining-tokens': '0',
125
- 'x-ratelimit-reset-tokens': '6ms',
126
- },
127
- };
128
- }
129
- function rateLimitTriggered(req: OpenAIRequest): boolean {
130
- const v = req.headers?.['x-twin-force-rate-limit'];
131
- return v === '1' || v === 'true';
132
- }
133
80
 
134
81
  function nowEpoch(occurredAt?: string): number {
135
82
  return Math.floor((occurredAt ? Date.parse(occurredAt) : 0) / 1000);
136
83
  }
137
- function nowIso(occurredAt?: string): string {
138
- return occurredAt ? new Date(Date.parse(occurredAt)).toISOString() : '1970-01-01T00:00:00.000Z';
139
- }
140
-
141
- // ── kernel helpers ──────────────────────────────────────────────────────────────────────
142
- function rows(type: string, root?: string): Array<Record<string, unknown>> {
143
- return projectResources(SERVICE, root).filter((r) => r.type === type);
144
- }
145
- function nextId(type: string, prefix: string, root?: string): string {
146
- let max = 0;
147
- for (const r of rows(type, root)) {
148
- const m = new RegExp(`^${prefix}-twin-(\\d+)$`).exec(String(r.id));
149
- if (m) max = Math.max(max, Number(m[1]));
150
- }
151
- return `${prefix}-twin-${max + 1}`;
152
- }
153
- function getRow(type: string, id: string, root?: string): Record<string, unknown> | undefined {
154
- return rows(type, root).find((r) => r.id === id);
155
- }
156
- // The kernel reserves the field name `type` as its resource-type discriminator, so the stored
157
- // vendor `object` field survives projection but the kernel `type` shadows nothing — we strip
158
- // the kernel housekeeping fields and re-shape the served view per resource.
159
- function strip(r: Record<string, unknown>): Record<string, unknown> {
160
- const out: Record<string, unknown> = {};
161
- for (const [k, v] of Object.entries(r)) {
162
- // drop the kernel housekeeping field + the twin's private underscore-prefixed fields.
163
- if (k === 'type' || k === 'updatedAt' || k.startsWith('_')) continue;
164
- out[k] = v;
165
- }
166
- return out;
167
- }
168
-
169
- // ── idempotency (Idempotency-Key header dedup) ───────────────────────────────────────────
170
- // A mutation carrying an Idempotency-Key is replayed verbatim on re-issue with the same key — the
171
- // vendor returns the original response (same id) and never re-applies the side effect. The twin
172
- // stores the serialized {status,body,headers} keyed by a hash of the key (type-prefixed id, so it
173
- // can't collide with another resource's numeric id — kernel (type,id) caveat).
174
- function idemId(key: string): string {
175
- let h = 0x811c9dc5;
176
- for (let i = 0; i < key.length; i++) { h ^= key.charCodeAt(i); h = Math.imul(h, 0x01000193); }
177
- return `idem-${(h >>> 0).toString(36)}`;
178
- }
179
- function getIdempotentResult(key: string, root?: string): OpenAIResponseEnvelope | null {
180
- const row = getRow('idempotency_record', idemId(key), root);
181
- if (!row || typeof row._result !== 'string') return null;
182
- try { return JSON.parse(row._result as string) as OpenAIResponseEnvelope; } catch { return null; }
183
- }
184
- async function storeIdempotentResult(key: string, result: OpenAIResponseEnvelope, req: OpenAIRequest): Promise<void> {
185
- const id = idemId(key);
186
- if (getRow('idempotency_record', id, req.root)) return; // a concurrent insert won
187
- await applyTwinWrite(SERVICE, {
188
- operation: 'idempotency_record.create',
189
- subjectType: 'idempotency_record',
190
- subjectId: id,
191
- fields: { object: 'idempotency_record', _key: key, _result: JSON.stringify(result) },
192
- ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
193
- actor: { kind: 'agent' },
194
- }, req.root);
195
- }
196
84
 
197
85
  // ── usage ledger (real recorded usage → the usage/costs reporting endpoints) ──────────────
198
86
  // Every billable inference call appends a usage record (model + token counts + a synthetic cost
@@ -215,60 +103,121 @@ const MODEL_PRICES: Record<string, { input: number; output: number }> = {
215
103
  function modelPrice(model: string): { input: number; output: number } {
216
104
  return MODEL_PRICES[model] ?? { input: 0, output: 0 };
217
105
  }
218
- function usageCost(model: string, inputTokens: number, outputTokens: number): number {
106
+ export function usageCost(model: string, inputTokens: number, outputTokens: number): number {
219
107
  const p = modelPrice(model);
220
108
  return (inputTokens / 1e6) * p.input + (outputTokens / 1e6) * p.output;
221
109
  }
222
- async function recordUsage(req: OpenAIRequest, kind: string, model: string, inputTokens: number, outputTokens: number): Promise<void> {
223
- const id = nextId('usage_record', 'usage', req.root);
224
- await applyTwinWrite(SERVICE, {
225
- operation: 'usage_record.create',
226
- subjectType: 'usage_record',
227
- subjectId: id,
228
- fields: {
229
- object: 'usage_record',
230
- kind,
231
- model,
232
- input_tokens: inputTokens,
233
- output_tokens: outputTokens,
234
- num_model_requests: 1,
235
- cost_usd: usageCost(model, inputTokens, outputTokens),
236
- created_at: nowEpoch(req.occurredAt),
237
- },
238
- ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
239
- actor: { kind: 'agent' },
240
- }, req.root);
241
- }
242
110
 
243
- // ── cursor pagination (after/limit + has_more, vendor-faithful list envelope) ────────────
244
- // OpenAI list endpoints page by an opaque cursor: `after=<id>` returns items AFTER that id and
245
- // `limit` (default 20, capped 100) caps the page; the envelope carries has_more/first_id/last_id.
246
- function paginate(items: Array<Record<string, unknown>>, rawPath: string): Record<string, unknown> {
247
- const qIdx = rawPath.indexOf('?');
248
- const qs = new URLSearchParams(qIdx >= 0 ? rawPath.slice(qIdx + 1) : '');
249
- const after = qs.get('after');
250
- let limit = Number(qs.get('limit'));
251
- if (!Number.isInteger(limit) || limit < 1) limit = 20;
252
- if (limit > 100) limit = 100;
253
- let start = 0;
254
- if (after) {
255
- const i = items.findIndex((it) => it.id === after);
256
- start = i >= 0 ? i + 1 : items.length; // unknown cursor → empty page (vendor returns nothing after end)
257
- }
258
- const page = items.slice(start, start + limit);
259
- const hasMore = start + limit < items.length;
260
- return { object: 'list', data: page, has_more: hasMore, first_id: page[0]?.id ?? null, last_id: page[page.length - 1]?.id ?? null };
261
- }
111
+ // ── Usage and costs (computed over the recorded usage rows) ─────────────────────────────
112
+ // The vendor's Usage and Costs APIs return time buckets from `start_time` (to `end_time` or now), each
113
+ // `bucket_width` wide (Costs: only `1d`), `limit` buckets a page (Usage `1d`: 7 by default, at most 31;
114
+ // `1h`: 24, 168; `1m`: 60, 1440; Costs: 7, 180), with `next_page` when more remain; a bucket holds one
115
+ // result over its window, or one per group when `group_by` names `model` (Usage) or `line_item` (Costs)
116
+ // (https://platform.openai.com/docs/api-reference/usage/completions,
117
+ // https://platform.openai.com/docs/api-reference/usage/costs). The twin records a usage row per billable
118
+ // inference call (semantics/shared.ts recordUsage) and aggregates them here: nothing is hardcoded.
119
+ /** The query a report answers: its window, bucket width, page size, where the page starts, and its groups. */
120
+ export type ReportQuery = { start: number; end: number; width: number; limit: number; from: number; groupBy: string[] };
121
+
122
+ const WIDTHS: Record<string, { seconds: number; limit: number; max: number }> = {
123
+ '1m': { seconds: 60, limit: 60, max: 1440 },
124
+ '1h': { seconds: 3600, limit: 24, max: 168 },
125
+ '1d': { seconds: 86400, limit: 7, max: 31 },
126
+ };
262
127
 
263
- // ── request parsing ─────────────────────────────────────────────────────────────────────
264
- function parseJson(body?: string): Record<string, unknown> {
265
- if (!body || !body.trim()) return {};
266
- try {
267
- const v = JSON.parse(body);
268
- return v && typeof v === 'object' ? (v as Record<string, unknown>) : {};
269
- } catch {
270
- return {};
271
- }
128
+ /** A report's query from its parameters; Costs buckets by day only and pages up to 180 of them. */
129
+ export function reportQuery(params: Record<string, unknown>, now: number, costs: boolean): ReportQuery {
130
+ const width = WIDTHS[String(params.bucket_width ?? '1d')] ?? WIDTHS['1d']!;
131
+ const max = costs ? 180 : width.max;
132
+ const asked = Number(params.limit);
133
+ const limit = Number.isInteger(asked) && asked >= 1 ? Math.min(asked, max) : width.limit;
134
+ const start = Number(params.start_time);
135
+ const end = params.end_time !== undefined ? Number(params.end_time) : now;
136
+ const page = /^page_(\d+)$/.exec(String(params.page ?? ''));
137
+ const groups = params.group_by;
138
+ return { start, end, width: width.seconds, limit, from: page ? Number(page[1]) : start, groupBy: (Array.isArray(groups) ? groups : groups === undefined ? [] : [groups]).map(String) };
139
+ }
140
+
141
+ /** Buckets of one page of the query, each holding `results(rows in its window)`. */
142
+ export function bucketed(records: Array<Record<string, unknown>>, q: ReportQuery, results: (rows: Array<Record<string, unknown>>, at: number, until: number) => unknown[]): Record<string, unknown> {
143
+ const data: unknown[] = [];
144
+ let at = q.from;
145
+ while (at < q.end && data.length < q.limit) {
146
+ const until = Math.min(at + q.width, q.end);
147
+ data.push({ object: 'bucket', start_time: at, end_time: until, results: results(records.filter((r) => Number(r.created_at) >= at && Number(r.created_at) < until), at, until) });
148
+ at = until;
149
+ }
150
+ return { object: 'page', data, has_more: at < q.end, next_page: at < q.end ? `page_${at}` : null };
151
+ }
152
+
153
+ /** Rows grouped by `key` when grouping by it, else all in one group keyed null; none when there are no rows. */
154
+ function grouped(rows: Array<Record<string, unknown>>, by: string | null): Array<[string | null, Array<Record<string, unknown>>]> {
155
+ const out = new Map<string | null, Array<Record<string, unknown>>>();
156
+ for (const r of rows) {
157
+ const k = by === null ? null : String(r[by]);
158
+ out.set(k, [...(out.get(k) ?? []), r]);
159
+ }
160
+ return [...out.entries()].sort((a, b) => (String(a[0]) < String(b[0]) ? -1 : 1));
161
+ }
162
+
163
+ /** What each usage kind's result counts, and the object it answers as (the spec's Usage*Result schemas;
164
+ * https://platform.openai.com/docs/api-reference/usage). A result names the grouping it answers (model, and for images
165
+ * their size and source) and null for the groupings not asked for. */
166
+ const USAGE_RESULTS: Record<string, { object: string; count: (rows: Array<Record<string, unknown>>) => Record<string, unknown>; by?: string[] }> = {
167
+ completions: {
168
+ object: 'organization.usage.completions.result',
169
+ // the twin caches nothing and hears, sees and speaks nothing: every token is uncached text
170
+ count: (rs) => {
171
+ const input = sum(rs, 'input_tokens');
172
+ const output = sum(rs, 'output_tokens');
173
+ return { input_tokens: input, input_cached_tokens: 0, input_cache_write_tokens: 0, input_uncached_tokens: input, output_tokens: output, input_text_tokens: input, output_text_tokens: output, input_cached_text_tokens: 0, input_audio_tokens: 0, input_cached_audio_tokens: 0, output_audio_tokens: 0, input_image_tokens: 0, input_cached_image_tokens: 0, output_image_tokens: 0, num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null, batch: null, service_tier: null };
174
+ },
175
+ },
176
+ embeddings: { object: 'organization.usage.embeddings.result', count: (rs) => ({ input_tokens: sum(rs, 'input_tokens'), num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null }) },
177
+ moderations: { object: 'organization.usage.moderations.result', count: (rs) => ({ input_tokens: sum(rs, 'input_tokens'), num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null }) },
178
+ audio_speeches: { object: 'organization.usage.audio_speeches.result', count: (rs) => ({ characters: sum(rs, 'characters'), num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null }) },
179
+ audio_transcriptions: { object: 'organization.usage.audio_transcriptions.result', count: (rs) => ({ seconds: sum(rs, 'seconds'), num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null }) },
180
+ images: { object: 'organization.usage.images.result', by: ['size', 'source'], count: (rs) => ({ images: sum(rs, 'images'), num_model_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null }) },
181
+ code_interpreter_sessions: { object: 'organization.usage.code_interpreter_sessions.result', count: (rs) => ({ num_sessions: sum(rs, 'num_sessions') }) },
182
+ file_search_calls: { object: 'organization.usage.file_searches.result', count: (rs) => ({ num_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null, vector_store_id: null }) },
183
+ web_search_calls: { object: 'organization.usage.web_searches.result', count: (rs) => ({ num_model_requests: sum(rs, 'num_model_requests'), num_requests: sum(rs, 'num_model_requests'), user_id: null, api_key_id: null, context_level: null }) },
184
+ };
185
+ const sum = (rows: Array<Record<string, unknown>>, k: string): number => rows.reduce((n, r) => n + Number(r[k] ?? 0), 0);
186
+ /** The kinds whose results name a model. */
187
+ const MODELLESS = new Set(['code_interpreter_sessions', 'file_search_calls']);
188
+
189
+ /** The usage report over the recorded usage rows, for one kind ('completions', 'embeddings', …). */
190
+ export function usageReport(records: Array<Record<string, unknown>>, kind: string | undefined, q: ReportQuery): Record<string, unknown> {
191
+ const spec = USAGE_RESULTS[kind ?? 'completions'] ?? USAGE_RESULTS.completions!;
192
+ const byModel = q.groupBy.includes('model') && !MODELLESS.has(kind ?? '');
193
+ const extra = (spec.by ?? []).filter((k) => q.groupBy.includes(k));
194
+ return bucketed(records.filter((r) => kind === undefined || r.kind === kind), q, (rows) => groupedBy(rows, [...(byModel ? ['model'] : []), ...extra]).map(([key, rs]) => ({
195
+ object: spec.object,
196
+ ...spec.count(rs),
197
+ project_id: null,
198
+ ...(MODELLESS.has(kind ?? '') ? {} : { model: byModel ? key.model : null }),
199
+ ...(spec.by ? Object.fromEntries(spec.by.map((k) => [k, extra.includes(k) ? key[k] : null])) : {}),
200
+ })));
201
+ }
202
+
203
+ /** Rows grouped by the values of `keys`, in key order; one group of all rows when no key; none when there are no rows. */
204
+ function groupedBy(rows: Array<Record<string, unknown>>, keys: string[]): Array<[Record<string, unknown>, Array<Record<string, unknown>>]> {
205
+ const out = new Map<string, [Record<string, unknown>, Array<Record<string, unknown>>]>();
206
+ for (const r of rows) {
207
+ const key = Object.fromEntries(keys.map((k) => [k, r[k] === undefined ? null : String(r[k])]));
208
+ const id = JSON.stringify(key);
209
+ out.set(id, [key, [...(out.get(id)?.[1] ?? []), r]]);
210
+ }
211
+ return [...out.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([, v]) => v);
212
+ }
213
+ /** The costs report: the recorded usage priced per model, per bucket, by line item (usage kind) when grouped so. */
214
+ export function costsReport(records: Array<Record<string, unknown>>, q: ReportQuery): Record<string, unknown> {
215
+ const byLine = q.groupBy.includes('line_item');
216
+ return bucketed(records, q, (rows) => grouped(rows, byLine ? 'kind' : null).map(([line, rs]) => ({
217
+ object: 'organization.costs.result',
218
+ amount: { value: rs.reduce((n, r) => n + Number(r.cost_usd ?? 0), 0), currency: 'usd' },
219
+ line_item: line, project_id: null, api_key_id: null, quantity: null, quantity_unit: null,
220
+ })));
272
221
  }
273
222
 
274
223
  const SYSTEM_FINGERPRINT = 'fp_twin_stub';
@@ -294,118 +243,155 @@ type ChatArgs = {
294
243
  seed?: number;
295
244
  logitBias?: Record<string, number>;
296
245
  prediction?: string;
246
+ /** the reasoning effort a reasoning model spends (the request's `reasoning_effort`, else the default) */
247
+ reasoningEffort?: string;
297
248
  store: boolean;
298
249
  metadata?: Record<string, unknown>;
299
250
  /** modalities: ['text'] (default) or ['text','audio'] — audio asks for a spoken output. */
300
251
  audioOutput?: { voice: string; format: string };
301
252
  };
302
253
 
303
- function validateChat(params: Record<string, unknown>): { args: ChatArgs } | { error: OpenAIResponseEnvelope } {
254
+ /** The roles a chat message may have (the spec's ChatCompletionRequestMessage variants). */
255
+ const ROLES = new Set(['developer', 'system', 'user', 'assistant', 'tool', 'function']);
256
+
257
+ /** OpenAI's refusal of a chat message whose role is not one of the request message types
258
+ * (https://platform.openai.com/docs/api-reference/chat/create, `messages`). */
259
+ function invalidRole(): { error: OpenAIResponseEnvelope } {
260
+ return { error: invalidRequest("each message must have a valid 'role'", 'messages') };
261
+ }
262
+
263
+ type ChatRefusal = { error: OpenAIResponseEnvelope };
264
+ // The chat options below are OpenAI's (https://platform.openai.com/docs/api-reference/chat/create); each
265
+ // parser answers the option's value or OpenAI's refusal of it.
266
+
267
+ /** `n`: how many choices to generate, at least one. */
268
+ function chatN(raw: unknown): number | ChatRefusal {
269
+ const n = Number(raw);
270
+ return Number.isInteger(n) && n >= 1 ? n : { error: invalidRequest("'n' must be an integer >= 1", 'n') };
271
+ }
272
+
273
+ /** `max_completion_tokens` (or the legacy `max_tokens`): a cap of at least one token. */
274
+ function chatMaxTokens(raw: unknown): number | ChatRefusal {
275
+ const max = Number(raw);
276
+ return Number.isInteger(max) && max >= 1 ? max : { error: invalidRequest("'max_tokens' must be an integer >= 1", 'max_tokens') };
277
+ }
278
+
279
+ /** `stop`: one sequence or a list of them. */
280
+ function chatStop(raw: unknown): string[] | ChatRefusal {
281
+ if (typeof raw === 'string') return [raw];
282
+ return Array.isArray(raw) ? (raw as string[]) : { error: invalidRequest("'stop' must be a string or array of strings", 'stop') };
283
+ }
284
+
285
+ /** `tool_choice`: 'auto' | 'none' | 'required' | { type:'function', function:{ name } }. */
286
+ function chatToolChoice(raw: unknown): ToolChoice | ChatRefusal {
287
+ if (raw === 'auto' || raw === 'none' || raw === 'required') return raw;
288
+ if (!raw || typeof raw !== 'object') return { error: invalidRequest("'tool_choice' must be 'auto'/'none'/'required' or a named function", 'tool_choice') };
289
+ const fn = (raw as { function?: { name?: unknown } }).function;
290
+ return typeof fn?.name === 'string' ? { name: fn.name } : { error: invalidRequest("invalid 'tool_choice' — named choice requires function.name", 'tool_choice') };
291
+ }
292
+
293
+ /** OpenAI's refusal of a `response_format` whose type is none of text, json_object and json_schema. */
294
+ function badResponseFormat(): ChatRefusal {
295
+ return { error: invalidRequest("'response_format.type' must be 'text', 'json_object', or 'json_schema'", 'response_format') };
296
+ }
297
+
298
+ /** `top_logprobs`: 0 to 20 alternatives per token, only with `logprobs: true`. */
299
+ function chatTopLogprobs(raw: unknown, logprobs: boolean): number | ChatRefusal {
300
+ const top = Number(raw);
301
+ if (!Number.isInteger(top) || top < 0 || top > 20) return { error: invalidRequest("'top_logprobs' must be an integer between 0 and 20", 'top_logprobs') };
302
+ return logprobs ? top : { error: invalidRequest("'top_logprobs' requires 'logprobs' to be true", 'top_logprobs') };
303
+ }
304
+
305
+ /** `seed`: an integer for best-effort reproducible sampling. */
306
+ function chatSeed(raw: unknown): number | ChatRefusal {
307
+ const seed = Number(raw);
308
+ return Number.isInteger(seed) ? seed : { error: invalidRequest("'seed' must be an integer", 'seed') };
309
+ }
310
+
311
+ /** `logit_bias`: token ids mapped to a bias from -100 to 100. */
312
+ function chatLogitBias(raw: unknown): Record<string, number> | ChatRefusal {
313
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { error: invalidRequest("'logit_bias' must be an object mapping token ids to bias values", 'logit_bias') };
314
+ const bias: Record<string, number> = {};
315
+ for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
316
+ const num = Number(v);
317
+ if (!Number.isFinite(num) || num < -100 || num > 100) return { error: invalidRequest("each 'logit_bias' value must be a number between -100 and 100", 'logit_bias') };
318
+ bias[k] = num;
319
+ }
320
+ return bias;
321
+ }
322
+
323
+ /** `prediction`: predicted output, `{ type: 'content', content }`, its content a string or text parts. */
324
+ function chatPrediction(raw: unknown): string | ChatRefusal {
325
+ const p = raw as { type?: unknown; content?: unknown } | undefined;
326
+ if (!p || typeof p !== 'object' || p.type !== 'content' || p.content === undefined) return { error: invalidRequest("'prediction' must be an object with type 'content' and a content field", 'prediction') };
327
+ if (typeof p.content === 'string') return p.content;
328
+ return Array.isArray(p.content) ? p.content.map((c) => (c && typeof c === 'object' ? String((c as { text?: unknown }).text ?? '') : String(c))).join('') : '';
329
+ }
330
+
331
+ /** `modalities` with `audio`: a spoken answer needs `audio: { voice, format }` (the vendor's rule). The twin
332
+ * can't synthesize speech, so the returned bytes are a labeled stub; the envelope is faithful. */
333
+ function chatAudio(modalities: unknown, audio: unknown): { voice: string; format: string } | ChatRefusal | undefined {
334
+ if (!Array.isArray(modalities)) return { error: invalidRequest("'modalities' must be an array", 'modalities') };
335
+ if (!(modalities as unknown[]).includes('audio')) return undefined;
336
+ const a = audio as { voice?: unknown; format?: unknown } | undefined;
337
+ if (!a || typeof a !== 'object' || typeof a.voice !== 'string' || typeof a.format !== 'string') return { error: invalidRequest("'audio' with a 'voice' and 'format' is required when 'modalities' includes 'audio'", 'audio') };
338
+ return { voice: a.voice, format: a.format };
339
+ }
340
+
341
+ export function validateChat(params: Record<string, unknown>): { args: ChatArgs } | { error: OpenAIResponseEnvelope } {
304
342
  if (params.model === undefined || params.model === '') return { error: invalidRequest("you must provide a model parameter", 'model') };
305
343
  if (typeof params.model !== 'string') return { error: invalidRequest("'model' must be a string", 'model') };
306
344
  if (!Array.isArray(params.messages)) return { error: invalidRequest("you must provide a messages parameter", 'messages') };
307
345
  if (params.messages.length === 0) return { error: invalidRequest("[] is too short - 'messages'", 'messages') };
308
346
  const messages = params.messages as ChatMessageParam[];
309
- for (const m of messages) {
310
- if (!m || typeof m !== 'object' || typeof m.role !== 'string') {
311
- return { error: invalidRequest("each message must have a valid 'role'", 'messages') };
312
- }
313
- }
314
- let n = 1;
315
- if (params.n !== undefined) {
316
- n = Number(params.n);
317
- if (!Number.isInteger(n) || n < 1) return { error: invalidRequest("'n' must be an integer >= 1", 'n') };
318
- }
347
+ if (messages.some((m) => !m || typeof m !== 'object' || !ROLES.has(String(m.role)))) return invalidRole();
348
+ // Each option the request may carry is parsed by its own function (below): a function returns the
349
+ // option's value, or OpenAI's refusal of it as `{ error }`.
350
+ const n = params.n === undefined ? 1 : chatN(params.n);
351
+ if (typeof n !== 'number') return n;
319
352
  // max_completion_tokens is the current name; max_tokens is the legacy alias.
320
353
  const maxRaw = params.max_completion_tokens ?? params.max_tokens;
321
- let maxTokens: number | undefined;
322
- if (maxRaw !== undefined) {
323
- maxTokens = Number(maxRaw);
324
- if (!Number.isInteger(maxTokens) || maxTokens < 1) return { error: invalidRequest("'max_tokens' must be an integer >= 1", 'max_tokens') };
325
- }
326
- const stopRaw = params.stop;
327
- let stop: string[] | undefined;
328
- if (stopRaw !== undefined) {
329
- if (typeof stopRaw === 'string') stop = [stopRaw];
330
- else if (Array.isArray(stopRaw)) stop = stopRaw as string[];
331
- else return { error: invalidRequest("'stop' must be a string or array of strings", 'stop') };
332
- }
333
- // tool_choice: 'auto' | 'none' | 'required' | { type:'function', function:{ name } }.
334
- let toolChoice: ToolChoice | undefined;
335
- const tcRaw = params.tool_choice;
336
- if (tcRaw !== undefined) {
337
- if (tcRaw === 'auto' || tcRaw === 'none' || tcRaw === 'required') toolChoice = tcRaw;
338
- else if (tcRaw && typeof tcRaw === 'object') {
339
- const fn = (tcRaw as { function?: { name?: unknown } }).function;
340
- if (typeof fn?.name === 'string') toolChoice = { name: fn.name };
341
- else return { error: invalidRequest("invalid 'tool_choice' — named choice requires function.name", 'tool_choice') };
342
- } else return { error: invalidRequest("'tool_choice' must be 'auto'/'none'/'required' or a named function", 'tool_choice') };
343
- }
354
+ const maxTokens = maxRaw === undefined ? undefined : chatMaxTokens(maxRaw);
355
+ if (typeof maxTokens === 'object') return maxTokens;
356
+ const stop = params.stop === undefined ? undefined : chatStop(params.stop);
357
+ if (stop && !Array.isArray(stop)) return stop;
358
+ const toolChoice = params.tool_choice === undefined ? undefined : chatToolChoice(params.tool_choice);
359
+ if (toolChoice && typeof toolChoice === 'object' && 'error' in toolChoice) return toolChoice;
344
360
  // response_format: { type:'text' | 'json_object' | 'json_schema', json_schema? }.
345
361
  let responseFormat: ResponseFormat = { kind: 'text' };
346
362
  const rf = params.response_format;
347
363
  if (rf !== undefined) {
348
364
  if (!rf || typeof rf !== 'object') return { error: invalidRequest("'response_format' must be an object", 'response_format') };
349
- const t = (rf as { type?: unknown }).type;
350
- if (t === 'json_object') responseFormat = { kind: 'json_object' };
351
- else if (t === 'json_schema') responseFormat = { kind: 'json_schema', schema: (rf as { json_schema?: unknown }).json_schema };
352
- else if (t === 'text' || t === undefined) responseFormat = { kind: 'text' };
353
- else return { error: invalidRequest("'response_format.type' must be 'text', 'json_object', or 'json_schema'", 'response_format') };
365
+ const t = (rf as { type?: unknown }).type ?? 'text';
366
+ const format: ResponseFormat | undefined = t === 'json_schema' ? { kind: 'json_schema', schema: (rf as { json_schema?: unknown }).json_schema } : t === 'json_object' || t === 'text' ? { kind: t } : undefined;
367
+ if (!format) return badResponseFormat();
368
+ responseFormat = format;
354
369
  }
355
370
  // stream_options.include_usage → emit a final usage-only chunk in the stream.
356
371
  const so = params.stream_options as { include_usage?: unknown } | undefined;
357
372
  const includeUsage = !!(so && typeof so === 'object' && so.include_usage === true);
358
373
  // logprobs (boolean) + top_logprobs (0..20, requires logprobs:true) → per-token logprob detail.
359
374
  const logprobs = params.logprobs === true;
360
- let topLogprobs: number | undefined;
361
- if (params.top_logprobs !== undefined) {
362
- topLogprobs = Number(params.top_logprobs);
363
- if (!Number.isInteger(topLogprobs) || topLogprobs < 0 || topLogprobs > 20) return { error: invalidRequest("'top_logprobs' must be an integer between 0 and 20", 'top_logprobs') };
364
- if (!logprobs) return { error: invalidRequest("'top_logprobs' requires 'logprobs' to be true", 'top_logprobs') };
365
- }
375
+ const topLogprobs = params.top_logprobs === undefined ? undefined : chatTopLogprobs(params.top_logprobs, logprobs);
376
+ if (typeof topLogprobs === 'object') return topLogprobs;
366
377
  // seed → reproducible sampling (the twin is already deterministic; we echo it via fingerprint).
367
- let seed: number | undefined;
368
- if (params.seed !== undefined) {
369
- seed = Number(params.seed);
370
- if (!Number.isInteger(seed)) return { error: invalidRequest("'seed' must be an integer", 'seed') };
371
- }
372
- // logit_bias → a map of token-id → bias in [-100, 100]; must be an object of numbers.
373
- let logitBias: Record<string, number> | undefined;
374
- if (params.logit_bias !== undefined) {
375
- const lb = params.logit_bias;
376
- if (!lb || typeof lb !== 'object' || Array.isArray(lb)) return { error: invalidRequest("'logit_bias' must be an object mapping token ids to bias values", 'logit_bias') };
377
- logitBias = {};
378
- for (const [k, v] of Object.entries(lb as Record<string, unknown>)) {
379
- const num = Number(v);
380
- if (!Number.isFinite(num) || num < -100 || num > 100) return { error: invalidRequest("each 'logit_bias' value must be a number between -100 and 100", 'logit_bias') };
381
- logitBias[k] = num;
382
- }
383
- }
378
+ const seed = params.seed === undefined ? undefined : chatSeed(params.seed);
379
+ if (typeof seed === 'object') return seed;
380
+ // logit_bias → a map of token-id → bias in [-100, 100].
381
+ const logitBias = params.logit_bias === undefined ? undefined : chatLogitBias(params.logit_bias);
382
+ if (logitBias && 'error' in logitBias) return logitBias as { error: OpenAIResponseEnvelope };
384
383
  // prediction → predicted outputs ({ type:'content', content }); content may be a string or parts.
385
- let prediction: string | undefined;
386
- if (params.prediction !== undefined) {
387
- const p = params.prediction as { type?: unknown; content?: unknown } | undefined;
388
- if (!p || typeof p !== 'object' || p.type !== 'content' || p.content === undefined) return { error: invalidRequest("'prediction' must be an object with type 'content' and a content field", 'prediction') };
389
- prediction = typeof p.content === 'string'
390
- ? p.content
391
- : Array.isArray(p.content) ? p.content.map((c) => (c && typeof c === 'object' ? String((c as { text?: unknown }).text ?? '') : String(c))).join('') : '';
392
- }
393
- // modalities + audio → audio output. modalities is ['text'] (default) or includes 'audio'; when
394
- // it does, `audio: { voice, format }` is REQUIRED (vendor rule). The twin can't synthesize speech,
395
- // so the returned bytes are a labeled stub — the envelope (message.audio shape) is faithful.
396
- let audioOutput: { voice: string; format: string } | undefined;
397
- const modalities = params.modalities;
398
- if (modalities !== undefined) {
399
- if (!Array.isArray(modalities)) return { error: invalidRequest("'modalities' must be an array", 'modalities') };
400
- if ((modalities as unknown[]).includes('audio')) {
401
- const a = params.audio as { voice?: unknown; format?: unknown } | undefined;
402
- if (!a || typeof a !== 'object' || typeof a.voice !== 'string' || typeof a.format !== 'string') {
403
- return { error: invalidRequest("'audio' with a 'voice' and 'format' is required when 'modalities' includes 'audio'", 'audio') };
404
- }
405
- audioOutput = { voice: a.voice, format: a.format };
406
- }
407
- }
384
+ const prediction = params.prediction === undefined ? undefined : chatPrediction(params.prediction);
385
+ if (typeof prediction === 'object') return prediction;
386
+ // modalities + audio → audio output.
387
+ const audioOutput = params.modalities === undefined ? undefined : chatAudio(params.modalities, params.audio);
388
+ if (audioOutput && 'error' in audioOutput) return audioOutput;
408
389
  // store + metadata → stored completions (retrievable later); metadata must be a flat object.
390
+ // a model refuses what its page says it does not take (openai-models.ts); a reasoning model reasons with the effort
391
+ // asked for, else the spec's default
392
+ const refused = unsupported(params.model, { effort: params.reasoning_effort, effortParam: 'reasoning_effort', temperature: params.temperature, top_p: params.top_p, logprobs: params.logprobs, chatTools: params.tools ?? params.functions });
393
+ if (refused) return { error: invalidRequest(refused.message, refused.param, 'unsupported_value') };
394
+ const reasoningEffort = typeof params.reasoning_effort === 'string' ? params.reasoning_effort : reasons(params.model) ? DEFAULT_EFFORT : undefined;
409
395
  const store = params.store === true;
410
396
  let metadata: Record<string, unknown> | undefined;
411
397
  if (params.metadata !== undefined) {
@@ -429,15 +415,51 @@ function validateChat(params: Record<string, unknown>): { args: ChatArgs } | { e
429
415
  logprobs,
430
416
  ...(topLogprobs !== undefined ? { topLogprobs } : {}),
431
417
  ...(seed !== undefined ? { seed } : {}),
432
- ...(logitBias !== undefined ? { logitBias } : {}),
418
+ ...(logitBias !== undefined ? { logitBias: logitBias as Record<string, number> } : {}),
433
419
  ...(prediction !== undefined ? { prediction } : {}),
420
+ ...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
434
421
  store,
435
422
  ...(metadata !== undefined ? { metadata } : {}),
436
- ...(audioOutput !== undefined ? { audioOutput } : {}),
423
+ ...(audioOutput !== undefined ? { audioOutput: audioOutput as { voice: string; format: string } } : {}),
437
424
  },
438
425
  };
439
426
  }
440
427
 
428
+ /** The text cut at the EARLIEST-occurring stop sequence across the whole `stop` list, not whichever is
429
+ * listed first: the real vendor's "stop generation at the first hit", regardless of array order. */
430
+ function stoppedAt(text: string, stops: string[]): string {
431
+ let stopAt = -1;
432
+ for (const s of stops) {
433
+ if (!s) continue;
434
+ const i = text.indexOf(s);
435
+ if (i >= 0 && (stopAt < 0 || i < stopAt)) stopAt = i;
436
+ }
437
+ return stopAt >= 0 ? text.slice(0, stopAt) : text;
438
+ }
439
+
440
+ /** The text cut to a `max_completion_tokens` cap (about four characters a token); the choice finishes `length`. */
441
+ function cutAt(text: string, maxTokens: number): string {
442
+ return text.slice(0, maxTokens * 4);
443
+ }
444
+
445
+ /** modalities:['audio'] → the assistant replies with an audio object (content is null, the text lives in
446
+ * audio.transcript). The twin can't synthesize speech, so `data` is a labeled-stub base64 string; the shape
447
+ * (id/data/transcript/expires_at) is vendor-faithful. */
448
+ function audioChoice(args: ChatArgs, idx: number, transcript: string, logprobs: ChatChoice['logprobs'], finish: ChatChoice['finish_reason']): { choice: ChatChoice; completionTokens: number } {
449
+ const voice = args.audioOutput!;
450
+ const stubBytes = `[twin-stub:${args.model}] no real audio synthesis — voice=${voice.voice} format=${voice.format}; transcript: ${transcript}`;
451
+ const audio = {
452
+ id: `audio-twin-${stableSuffix(args)}-${idx}`,
453
+ data: Buffer.from(stubBytes, 'utf8').toString('base64'),
454
+ transcript,
455
+ expires_at: nowEpoch() + 3600,
456
+ };
457
+ return {
458
+ choice: { index: idx, message: { role: 'assistant', content: null, refusal: null, audio }, logprobs, finish_reason: finish },
459
+ completionTokens: estimateTokens(transcript),
460
+ };
461
+ }
462
+
441
463
  // Build ONE deterministic stub choice (index `idx`). Honors tools/functions (tool_calls +
442
464
  // finish_reason tool_calls), tool_choice (none/required/named), parallel_tool_calls,
443
465
  // response_format (json_object/json_schema), max_tokens, and stop sequences.
@@ -448,28 +470,21 @@ function buildChoice(args: ChatArgs, idx: number): { choice: ChatChoice; complet
448
470
  // forces it (even when the heuristic otherwise would not); 'auto'/default calls when tools exist.
449
471
  const forbidTools = args.toolChoice === 'none';
450
472
  const forcedName = typeof args.toolChoice === 'object' ? args.toolChoice.name : undefined;
451
- const wantTool = hasTools && !forbidTools;
473
+ // a model answers a tool's result in words unless the caller insists on another call; one that always
474
+ // called a tool would never let an app's tool loop end
475
+ const answeringTool = ['tool', 'function'].includes(String(args.messages.at(-1)?.role));
476
+ const wantTool = hasTools && !forbidTools && (!answeringTool || forcedName !== undefined || args.toolChoice === 'required');
452
477
  if (wantTool) {
453
478
  // parallel_tool_calls (default true) → the stub may emit one call per provided tool; a named
454
479
  // choice or parallel:false collapses to a single call.
455
- const list = Array.isArray(toolsSource) ? toolsSource : [];
456
- const calls: ChatToolCall[] = [];
457
- if (forcedName || args.parallelToolCalls === false) {
458
- const tc = stubToolCall(toolsSource, idx + 1, forcedName);
459
- if (tc) calls.push(tc);
460
- } else {
461
- for (let t = 0; t < list.length; t++) {
462
- const tc = stubToolCall([list[t]], idx * 100 + t + 1);
463
- if (tc) calls.push(tc);
464
- }
465
- }
466
- if (calls.length) {
467
- const completionTokens = estimateTokens(JSON.stringify(calls));
468
- return {
469
- choice: { index: idx, message: { role: 'assistant', content: null, tool_calls: calls, refusal: null }, logprobs: null, finish_reason: 'tool_calls' },
470
- completionTokens,
471
- };
472
- }
480
+ // (the tools are there, so each call is made)
481
+ const calls: ChatToolCall[] = forcedName || args.parallelToolCalls === false
482
+ ? [stubToolCall(toolsSource, idx + 1, forcedName, lastUserText(args.messages))!]
483
+ : (toolsSource as unknown[]).map((tool, t) => stubToolCall([tool], idx * 100 + t + 1, undefined, lastUserText(args.messages))!);
484
+ return {
485
+ choice: { index: idx, message: { role: 'assistant', content: null, tool_calls: calls, refusal: null }, logprobs: null, finish_reason: 'tool_calls' },
486
+ completionTokens: estimateTokens(JSON.stringify(calls)),
487
+ };
473
488
  }
474
489
  // json_mode: when response_format requests json_object/json_schema, the content is valid JSON.
475
490
  // prediction (predicted outputs): a real model uses the prediction to speed decoding but still
@@ -482,78 +497,57 @@ function buildChoice(args: ChatArgs, idx: number): { choice: ChatChoice; complet
482
497
  : args.responseFormat.kind === 'json_schema'
483
498
  ? stubJsonObject(args.messages, args.model, args.responseFormat.schema)
484
499
  : stubAssistantText(args.messages, args.model);
485
- let finish: ChatChoice['finish_reason'] = 'stop';
486
- // Truncate at the EARLIEST-occurring stop sequence across the whole `stop` list — not
487
- // whichever sequence happens to be listed first — so multi-element `stop` arrays match the
488
- // real vendor's "stop generation at the first hit" semantics regardless of array order.
489
- let stopAt = -1;
490
- for (const s of args.stop ?? []) {
491
- if (!s) continue;
492
- const i = text.indexOf(s);
493
- if (i >= 0 && (stopAt < 0 || i < stopAt)) stopAt = i;
494
- }
495
- if (stopAt >= 0) text = text.slice(0, stopAt);
496
- if (args.maxTokens !== undefined && estimateTokens(text) > args.maxTokens) {
497
- text = text.slice(0, args.maxTokens * 4);
498
- finish = 'length';
499
- }
500
+ if (args.stop) text = stoppedAt(text, args.stop);
501
+ const capped = args.maxTokens !== undefined && estimateTokens(text) > args.maxTokens;
502
+ if (capped) text = cutAt(text, args.maxTokens!);
503
+ const finish: ChatChoice['finish_reason'] = capped ? 'length' : 'stop';
500
504
  const logprobs = args.logprobs ? buildLogprobs(text, args.topLogprobs ?? 0) : null;
501
- // modalities:['audio'] → the assistant replies with an audio object (content is null, the text
502
- // lives in audio.transcript). The twin can't synthesize speech, so `data` is a labeled-stub
503
- // base64 string; the shape (id/data/transcript/expires_at) is vendor-faithful.
504
- if (args.audioOutput) {
505
- const transcript = text;
506
- const stubBytes = `[twin-stub:${args.model}] no real audio synthesis — voice=${args.audioOutput.voice} format=${args.audioOutput.format}; transcript: ${transcript}`;
507
- const audio = {
508
- id: `audio-twin-${stableSuffix(args)}-${idx}`,
509
- data: Buffer.from(stubBytes, 'utf8').toString('base64'),
510
- transcript,
511
- expires_at: nowEpoch() + 3600,
512
- };
513
- return {
514
- choice: { index: idx, message: { role: 'assistant', content: null, refusal: null, audio }, logprobs, finish_reason: finish },
515
- completionTokens: estimateTokens(transcript),
516
- };
517
- }
505
+ if (args.audioOutput) return audioChoice(args, idx, text, logprobs, finish);
518
506
  return {
519
- choice: { index: idx, message: { role: 'assistant', content: text, refusal: null }, logprobs, finish_reason: finish },
507
+ // a text answer carries its annotations (none: the twin cites no web source), as OpenAI's does
508
+ // (https://platform.openai.com/docs/api-reference/chat/object, `choices[].message.annotations`)
509
+ choice: { index: idx, message: { role: 'assistant', content: text, refusal: null, annotations: [] }, logprobs, finish_reason: finish },
520
510
  completionTokens: estimateTokens(text),
521
511
  };
522
512
  }
523
513
 
524
- function buildChatCompletion(args: ChatArgs, occurredAt?: string, scenarioEngine?: OpenAIScenarioEngine): ChatCompletion {
514
+ /** Predicted-output accounting: the twin echoes the prediction, so every predicted token is "accepted". */
515
+ function predictionUsage(prediction: string): Pick<NonNullable<ChatUsage['completion_tokens_details']>, 'accepted_prediction_tokens' | 'rejected_prediction_tokens'> {
516
+ return { accepted_prediction_tokens: estimateTokens(prediction), rejected_prediction_tokens: 0 };
517
+ }
518
+
519
+ export function buildChatCompletion(args: ChatArgs, occurredAt?: string, decision?: ScenarioDecision): ChatCompletion {
525
520
  const promptTokens = countPromptTokens(args.messages);
526
- // Scenario handlers: a fired handler scripts the assistant turn; a miss teaches in the stub.
527
- let scripted: ScriptedResult | null = null;
528
- let missTeach = '';
529
- if (scenarioEngine) {
530
- const decision = scenarioEngine.next({ model: args.model, messages: args.messages, tools: args.tools, maxTokens: args.maxTokens });
531
- if (decision.kind === 'handler') scripted = realizeOpenAIRespond(decision.respond as OpenAIScenarioRespond);
532
- else missTeach = `\n[twin-scenario miss — no handler matched. Author one in the world dir's handlers/openai.json (GET /twin explains; GET /twin/scenario lists handlers + misses). Features seen: ${JSON.stringify(decision.miss.features)}]`;
533
- }
521
+ // Scenario handlers: a fired handler scripts the assistant turn; a miss teaches in the stub. The
522
+ // decision was made (and any fault honored) by the request handler through the engine's serve(); this
523
+ // builder only realizes it — a status fault never reaches here.
524
+ const turn = decision && scenarioTurn(decision);
534
525
  const choices: ChatChoice[] = [];
535
526
  let completionTokens = 0;
536
527
  for (let i = 0; i < args.n; i++) {
537
- if (scripted) {
538
- const message = scripted.toolCalls.length
539
- ? { role: 'assistant' as const, content: scripted.text, tool_calls: scripted.toolCalls, refusal: null }
540
- : { role: 'assistant' as const, content: scripted.text ?? '', refusal: null };
541
- const ct = estimateTokens(JSON.stringify(scripted.toolCalls.length ? scripted.toolCalls : scripted.text ?? ''));
542
- choices.push({ index: i, message, logprobs: null, finish_reason: scripted.finishReason });
543
- completionTokens += ct;
544
- continue;
545
- }
546
- const { choice, completionTokens: ct } = buildChoice(args, i);
547
- if (missTeach && typeof choice.message.content === 'string') choice.message.content += missTeach;
528
+ const { choice, completionTokens: ct } = turn?.scripted ? scriptedChoice(turn.scripted, i) : buildChoice(args, i);
529
+ if (turn?.missTeach && typeof choice.message.content === 'string') choice.message.content += turn.missTeach;
548
530
  choices.push(choice);
549
531
  completionTokens += ct;
550
532
  }
551
- const usage: ChatUsage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens };
533
+ // usage breaks its counts down as OpenAI's does (https://platform.openai.com/docs/api-reference/chat/object,
534
+ // `usage.prompt_tokens_details`, `usage.completion_tokens_details`): the twin caches nothing and reasons and
535
+ // speaks nothing, so those are zero
536
+ const usage: ChatUsage = {
537
+ prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens,
538
+ prompt_tokens_details: { cached_tokens: 0, audio_tokens: 0 },
539
+ completion_tokens_details: { reasoning_tokens: 0, audio_tokens: 0, accepted_prediction_tokens: 0, rejected_prediction_tokens: 0 },
540
+ };
552
541
  // prediction (predicted outputs): real responses report how many predicted tokens were accepted
553
542
  // vs rejected. The twin echoes the prediction, so all predicted tokens are "accepted".
554
- if (args.prediction !== undefined) {
555
- const accepted = estimateTokens(args.prediction);
556
- usage.completion_tokens_details = { accepted_prediction_tokens: accepted, rejected_prediction_tokens: 0 };
543
+ if (args.prediction !== undefined) usage.completion_tokens_details = { ...usage.completion_tokens_details!, ...predictionUsage(args.prediction) };
544
+ // a reasoning model's reasoning tokens are billed as output tokens (https://developers.openai.com/api/docs/guides/reasoning:
545
+ // "they still occupy space in the model's context window and are billed as output tokens")
546
+ if (args.reasoningEffort !== undefined && reasons(args.model)) {
547
+ const spent = REASONING_BUDGET[args.reasoningEffort] ?? 0;
548
+ usage.completion_tokens += spent;
549
+ usage.total_tokens += spent;
550
+ usage.completion_tokens_details = { ...usage.completion_tokens_details!, reasoning_tokens: spent };
557
551
  }
558
552
  return {
559
553
  id: `chatcmpl-twin-${stableSuffix(args)}`,
@@ -563,27 +557,13 @@ function buildChatCompletion(args: ChatArgs, occurredAt?: string, scenarioEngine
563
557
  choices,
564
558
  usage,
565
559
  system_fingerprint: SYSTEM_FINGERPRINT,
560
+ // the tier that served the request: the standard one, which is what `auto` and `default` pick for a project
561
+ // not on Scale Tier (https://platform.openai.com/docs/api-reference/chat/object, `service_tier`)
562
+ service_tier: 'default',
566
563
  ...(args.metadata !== undefined ? { metadata: args.metadata } : {}),
567
564
  };
568
565
  }
569
566
 
570
- // Persist a stored chat completion (store:true) so it can be retrieved/listed/deleted later, and
571
- // keep the request messages for the /messages sub-resource (stored-completions input replay).
572
- async function storeChatCompletion(resp: ChatCompletion, args: ChatArgs, req: OpenAIRequest): Promise<void> {
573
- const inputMessages = args.messages.map((m, i) => ({ id: `${resp.id}-msg-${i}`, role: m.role, content: typeof m.content === 'string' ? m.content : (m.content ?? null) }));
574
- await applyTwinWrite(SERVICE, {
575
- operation: 'chat_completion.create',
576
- subjectType: 'chat_completion',
577
- subjectId: resp.id,
578
- fields: { ...resp, _stored: true, _input_messages: inputMessages },
579
- ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
580
- actor: { kind: 'agent' },
581
- }, req.root);
582
- }
583
- function storedChatView(r: Record<string, unknown>): Record<string, unknown> {
584
- return { id: r.id, ...strip(r) };
585
- }
586
-
587
567
  // A deterministic id suffix from the request (so ids are stable + assertable, like the twin's
588
568
  // other deterministic outputs). Hash of the prompt text + model + seed (seed changes sampling,
589
569
  // so it changes the response id the way a real seed-distinct request does).
@@ -602,26 +582,30 @@ function chunkText(text: string): string[] {
602
582
  return out;
603
583
  }
604
584
 
585
+ /** A streamed choice's tool calls: one tool_calls delta-pair per call, each carrying its own `index`
586
+ * (parallel tool calls). */
587
+ function streamToolCalls(calls: ChatToolCall[], idx: number, base: Record<string, unknown>, sink: SseSink): void {
588
+ calls.forEach((tc, tIdx) => {
589
+ sink({ data: { ...base, choices: [{ index: idx, delta: { tool_calls: [{ index: tIdx, id: tc.id, type: 'function', function: { name: tc.function.name, arguments: '' } }] }, logprobs: null, finish_reason: null }] } });
590
+ sink({ data: { ...base, choices: [{ index: idx, delta: { tool_calls: [{ index: tIdx, function: { arguments: tc.function.arguments } }] }, logprobs: null, finish_reason: null }] } });
591
+ });
592
+ }
593
+
605
594
  /**
606
595
  * Emit the vendor-faithful Chat Completions streaming sequence into the injected sink (NO
607
596
  * sockets, NO setTimeout). Real order: a first chunk with `delta:{role:'assistant'}`, then
608
597
  * `delta:{content}` chunks (or tool_calls deltas), then a final chunk with `finish_reason`,
609
598
  * then `[DONE]`. Deterministic + synchronous so a collector can assert the full sequence.
610
599
  */
611
- export function streamChat(args: ChatArgs, sink: SseSink, occurredAt?: string, scenarioEngine?: OpenAIScenarioEngine): ChatCompletion {
612
- const full = buildChatCompletion(args, occurredAt, scenarioEngine);
600
+ export function streamChat(args: ChatArgs, sink: SseSink, occurredAt?: string, decision?: ScenarioDecision): ChatCompletion {
601
+ const full = buildChatCompletion(args, occurredAt, decision);
613
602
  const base = { id: full.id, object: 'chat.completion.chunk' as const, created: full.created, model: full.model, system_fingerprint: SYSTEM_FINGERPRINT };
614
603
  for (const choice of full.choices) {
615
604
  const idx = choice.index;
616
605
  // role chunk
617
606
  sink({ data: { ...base, choices: [{ index: idx, delta: { role: 'assistant', content: '' }, logprobs: null, finish_reason: null }] } });
618
- if (choice.message.tool_calls && choice.message.tool_calls.length) {
619
- // one tool_calls delta-pair per call, each carrying its own `index` (parallel tool calls).
620
- choice.message.tool_calls.forEach((tc, tIdx) => {
621
- sink({ data: { ...base, choices: [{ index: idx, delta: { tool_calls: [{ index: tIdx, id: tc.id, type: 'function', function: { name: tc.function.name, arguments: '' } }] }, logprobs: null, finish_reason: null }] } });
622
- sink({ data: { ...base, choices: [{ index: idx, delta: { tool_calls: [{ index: tIdx, function: { arguments: tc.function.arguments } }] }, logprobs: null, finish_reason: null }] } });
623
- });
624
- } else {
607
+ if (choice.message.tool_calls && choice.message.tool_calls.length) streamToolCalls(choice.message.tool_calls, idx, base, sink);
608
+ else {
625
609
  for (const piece of chunkText(choice.message.content ?? '')) {
626
610
  sink({ data: { ...base, choices: [{ index: idx, delta: { content: piece }, logprobs: null, finish_reason: null }] } });
627
611
  }
@@ -637,7 +621,9 @@ export function streamChat(args: ChatArgs, sink: SseSink, occurredAt?: string, s
637
621
  }
638
622
 
639
623
  // ── Responses API ───────────────────────────────────────────────────────────────────────
640
- type ResponsesArgs = {
624
+ export type ResponsesArgs = {
625
+ inputItems: Record<string, unknown>[];
626
+ instructions?: string;
641
627
  model: string;
642
628
  inputText: string;
643
629
  messages: ChatMessageParam[];
@@ -648,31 +634,61 @@ type ResponsesArgs = {
648
634
  reasoningEffort?: string;
649
635
  /** background:true → the response is created `queued` and processed asynchronously (poll + cancel). */
650
636
  background: boolean;
637
+ /** The caller's tools, as sent (Responses shape `{type:'function', name, parameters}`); the scenario reads their names. */
638
+ tools?: unknown;
639
+ /** max_output_tokens, when sent — the output cap a scenario handler may key on. */
640
+ maxTokens?: number;
641
+ /** the request's settings a response answers back as sent, or OpenAI's defaults */
642
+ echo: { metadata: unknown; temperature: unknown; top_p: unknown; parallel_tool_calls: unknown; tool_choice: unknown; text: unknown; truncation: unknown; user: unknown; max_tool_calls: unknown; top_logprobs: unknown; service_tier: unknown };
643
+ /** reasoning.summary, when sent: a reasoning item carries a summary only when one is asked for */
644
+ reasoningSummary?: string;
651
645
  };
652
- const REASONING_EFFORTS = new Set(['minimal', 'low', 'medium', 'high']);
653
646
 
654
- function validateResponses(params: Record<string, unknown>): { args: ResponsesArgs } | { error: OpenAIResponseEnvelope } {
647
+ export function responseMessages(items: Record<string, unknown>[], instructions?: string): ChatMessageParam[] {
648
+ const messages: ChatMessageParam[] = instructions ? [{ role: 'system', content: instructions }] : [];
649
+ const partsText = (content: unknown): string => typeof content === 'string' ? content : Array.isArray(content) ? content.map((c) => c && typeof c === 'object' ? String(c.text ?? JSON.stringify(c)) : String(c)).join('\n') : '';
650
+ for (const item of items) {
651
+ if (item.type === 'function_call') {
652
+ messages.push({ role: 'assistant', content: '', tool_calls: [{ id: String(item.call_id ?? item.id ?? ''), type: 'function', function: { name: String(item.name ?? ''), arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {}) } }] });
653
+ } else if (item.type === 'function_call_output') {
654
+ messages.push({ role: 'tool', content: partsText(item.output), tool_call_id: String(item.call_id ?? '') });
655
+ } else if (item.type === 'message' || item.type === undefined) {
656
+ messages.push({ role: (item.role ?? 'user') as ChatMessageParam['role'], content: partsText(item.content) });
657
+ }
658
+ // Reasoning and other typed items remain in state, without inventing user turns.
659
+ }
660
+ return messages;
661
+ }
662
+
663
+ /** OpenAI's refusal of an `input` list holding something other than input items (objects)
664
+ * (https://platform.openai.com/docs/api-reference/responses/create, `input`). */
665
+ function itemsNotObjects(): { error: OpenAIResponseEnvelope } {
666
+ return { error: invalidRequest("'input' items must be objects", 'input') };
667
+ }
668
+
669
+ export function validateResponses(params: Record<string, unknown>): { args: ResponsesArgs } | { error: OpenAIResponseEnvelope } {
655
670
  if (params.model === undefined || params.model === '') return { error: invalidRequest("you must provide a model parameter", 'model') };
656
671
  if (typeof params.model !== 'string') return { error: invalidRequest("'model' must be a string", 'model') };
657
672
  if (params.input === undefined) return { error: invalidRequest("you must provide an input parameter", 'input') };
658
- // input may be a string OR an array of input items (role/content). Reduce to plain text +
659
- // a messages-shaped view for token counting.
660
- let inputText = '';
661
- const messages: ChatMessageParam[] = [];
673
+ // Keep vendor items as state; the messages view is only a projection for the scenario.
674
+ let inputItems: Record<string, unknown>[];
662
675
  if (typeof params.input === 'string') {
663
- inputText = params.input;
664
- messages.push({ role: 'user', content: params.input });
676
+ inputItems = [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: params.input }] }];
665
677
  } else if (Array.isArray(params.input)) {
666
- for (const item of params.input as Array<Record<string, unknown>>) {
667
- const role = (typeof item?.role === 'string' ? item.role : 'user') as ChatMessageParam['role'];
668
- const content = item?.content;
669
- const text = typeof content === 'string' ? content : Array.isArray(content) ? content.map((c) => (c && typeof c === 'object' ? String((c as { text?: unknown }).text ?? JSON.stringify(c)) : String(c))).join('\n') : '';
670
- inputText += (inputText ? '\n' : '') + text;
671
- messages.push({ role, content: text });
672
- }
678
+ if (params.input.some((item) => !item || typeof item !== 'object' || Array.isArray(item))) return itemsNotObjects();
679
+ inputItems = params.input.map((item: Record<string, unknown>) => {
680
+ if (item.type !== undefined && item.type !== 'message') return { ...item };
681
+ return { ...item, type: 'message', role: item.role ?? 'user', content: typeof item.content === 'string' ? [{ type: item.role === 'assistant' ? 'output_text' : 'input_text', text: item.content }] : item.content };
682
+ });
673
683
  } else {
674
684
  return { error: invalidRequest("'input' must be a string or an array of input items", 'input') };
675
685
  }
686
+ const instructions = typeof params.instructions === 'string' ? params.instructions : undefined;
687
+ const messages = responseMessages(inputItems, instructions);
688
+ const inputText = responseMessages(inputItems).map((m) => m.content).filter(Boolean).join('\n');
689
+ if (params.tools !== undefined && !Array.isArray(params.tools)) return { error: invalidRequest("'tools' must be an array", 'tools') };
690
+ const maxOut = params.max_output_tokens;
691
+ if (maxOut !== undefined && (typeof maxOut !== 'number' || !Number.isInteger(maxOut) || maxOut < 1)) return { error: invalidRequest("'max_output_tokens' must be a positive integer", 'max_output_tokens') };
676
692
  const prev = params.previous_response_id;
677
693
  if (prev !== undefined && (typeof prev !== 'string' || !prev)) return { error: invalidRequest("'previous_response_id' must be a string", 'previous_response_id') };
678
694
  // reasoning.effort → the model spends a (stubbed) reasoning budget; the item shape is faithful.
@@ -681,14 +697,19 @@ function validateResponses(params: Record<string, unknown>): { args: ResponsesAr
681
697
  const r = params.reasoning;
682
698
  if (!r || typeof r !== 'object' || Array.isArray(r)) return { error: invalidRequest("'reasoning' must be an object", 'reasoning') };
683
699
  const effort = (r as { effort?: unknown }).effort;
684
- if (effort !== undefined) {
685
- if (typeof effort !== 'string' || !REASONING_EFFORTS.has(effort)) return { error: invalidRequest("'reasoning.effort' must be one of 'minimal', 'low', 'medium', 'high'", 'reasoning.effort') };
686
- reasoningEffort = effort;
687
- }
688
- }
700
+ if (effort !== undefined && effort !== null) reasoningEffort = effort as string;
701
+ }
702
+ // a model refuses what its page says it does not take (openai-models.ts); a reasoning model reasons with the effort
703
+ // asked for, else the spec's default
704
+ const refused = unsupported(params.model, { effort: reasoningEffort, effortParam: 'reasoning.effort', temperature: params.temperature, top_p: params.top_p });
705
+ if (refused) return { error: invalidRequest(refused.message, refused.param, 'unsupported_value') };
706
+ if (reasoningEffort === undefined && reasons(params.model)) reasoningEffort = DEFAULT_EFFORT;
707
+ const reasoningSummary = params.reasoning && typeof (params.reasoning as { summary?: unknown }).summary === 'string' ? String((params.reasoning as { summary: string }).summary) : undefined;
689
708
  return {
690
709
  args: {
691
710
  model: params.model,
711
+ inputItems,
712
+ instructions,
692
713
  inputText,
693
714
  messages,
694
715
  stream: params.stream === true,
@@ -696,143 +717,205 @@ function validateResponses(params: Record<string, unknown>): { args: ResponsesAr
696
717
  background: params.background === true,
697
718
  ...(typeof prev === 'string' ? { previousResponseId: prev } : {}),
698
719
  ...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
720
+ ...(Array.isArray(params.tools) ? { tools: params.tools } : {}),
721
+ ...(typeof maxOut === 'number' ? { maxTokens: maxOut } : {}),
722
+ ...(reasoningSummary !== undefined ? { reasoningSummary } : {}),
723
+ // OpenAI's defaults for what was not sent (the Response schema's): temperature and top_p 1, tools in parallel, auto;
724
+ // plain text out, no truncation, no end user, no tool-call cap, no alternative tokens, the default tier
725
+ // (https://platform.openai.com/docs/api-reference/responses/object)
726
+ echo: {
727
+ metadata: params.metadata ?? {}, temperature: params.temperature ?? 1, top_p: params.top_p ?? 1, parallel_tool_calls: params.parallel_tool_calls ?? true, tool_choice: params.tool_choice ?? 'auto',
728
+ text: params.text ?? { format: { type: 'text' } }, truncation: params.truncation ?? 'disabled', user: params.user ?? null, max_tool_calls: params.max_tool_calls ?? null,
729
+ top_logprobs: params.top_logprobs ?? 0, service_tier: ['flex', 'priority', 'scale'].includes(String(params.service_tier)) ? params.service_tier : 'default',
730
+ },
699
731
  },
700
732
  };
701
733
  }
702
734
 
703
- // A deterministic reasoning-token budget per effort level (more effort → more reasoning tokens).
704
- const REASONING_BUDGET: Record<string, number> = { minimal: 8, low: 16, medium: 48, high: 128 };
735
+ /** A tool as a response answers it: as sent, with what OpenAI fills in for what was not. A web search preview searches
736
+ * with `medium` context from the United States unless told otherwise (the spec's WebSearchPreviewTool: "`medium` is
737
+ * the default"; `user_location` "If omitted or null, defaults to the United States"); a function tool sent without
738
+ * `strict` answers strict, as the reference's Functions example answers the tool it sends; a file search tool, its
739
+ * default filter and ranking
740
+ * (https://platform.openai.com/docs/api-reference/responses/create). */
741
+ const TOOL_DEFAULTS: Record<string, (t: Record<string, unknown>) => Record<string, unknown>> = {
742
+ function: (t) => ({ ...t, strict: t.strict ?? true }),
743
+ // a file search sent without filters or ranking answers no filter and the automatic ranker with no threshold, as the
744
+ // reference's File search example answers the tool it sends
745
+ file_search: (t) => ({ ...t, filters: t.filters ?? null, ranking_options: t.ranking_options ?? { ranker: 'auto', score_threshold: 0 } }),
746
+ // and the domains it is limited to, none unless sent, as the reference's Web search example answers `"domains": []`
747
+ web_search_preview: (t) => ({ ...t, domains: t.domains ?? [], search_context_size: t.search_context_size ?? 'medium', user_location: t.user_location ?? { type: 'approximate', city: null, country: 'US', region: null, timezone: null } }),
748
+ };
749
+ TOOL_DEFAULTS.web_search_preview_2025_03_11 = TOOL_DEFAULTS.web_search_preview!;
750
+ function answeredTool(tool: unknown): unknown {
751
+ const t = (tool && typeof tool === 'object' ? tool : {}) as Record<string, unknown>;
752
+ return TOOL_DEFAULTS[String(t.type)]?.(t) ?? tool;
753
+ }
754
+
755
+ /** The built-in tool calls a response makes: a web search for a web search tool, a file search over the named stores for a
756
+ * file search tool, each querying the input's text (its results are not included unless asked for: `results` null). */
757
+ function builtInCalls(args: ResponsesArgs, suffix: string): ResponseOutputItem[] {
758
+ const query = args.inputText.slice(0, 200);
759
+ const out: ResponseOutputItem[] = [];
760
+ for (const [i, tool] of (Array.isArray(args.tools) ? (args.tools as Array<{ type?: unknown }>) : []).entries()) {
761
+ const type = String(tool?.type ?? '');
762
+ if (/^web_search/.test(type)) out.push({ type: 'web_search_call', id: `ws-twin-${suffix}-${i}`, status: 'completed', action: { type: 'search', query } });
763
+ if (type === 'file_search') out.push({ type: 'file_search_call', id: `fs-twin-${suffix}-${i}`, status: 'completed', queries: [query], results: null });
764
+ }
765
+ return out;
766
+ }
705
767
 
706
- function buildResponse(args: ResponsesArgs, occurredAt?: string, idSuffix?: string): OpenAIResponse {
707
- const text = stubAssistantText(args.messages, args.model);
708
- const inputTokens = countPromptTokens(args.messages);
709
- const messageTokens = estimateTokens(text);
768
+ /** What every response answers besides its output: the request's settings as sent (or OpenAI's defaults), no error,
769
+ * no program access (https://platform.openai.com/docs/api-reference/responses/object). */
770
+ const responseSettings = (args: ResponsesArgs): Record<string, unknown> => ({
771
+ instructions: args.instructions ?? null, tools: Array.isArray(args.tools) ? args.tools.map(answeredTool) : [], ...args.echo,
772
+ access_programs: null, error: null, incomplete_details: null,
773
+ // whether it is kept: the spec's own Response example answers `"store": true` (spec/patches.json)
774
+ background: args.background, store: args.store, max_output_tokens: args.maxTokens ?? null,
775
+ previous_response_id: args.previousResponseId ?? null,
776
+ reasoning: { effort: args.reasoningEffort ?? null, summary: args.reasoningSummary ?? null },
777
+ });
778
+
779
+ // A deterministic reasoning-token budget per effort level (more effort → more reasoning tokens).
780
+ const REASONING_BUDGET: Record<string, number> = { none: 0, minimal: 8, low: 16, medium: 48, high: 128, xhigh: 256, max: 512 };
781
+
782
+ /** The function call an unscripted response makes: when the caller requires one or names one, or on
783
+ * `auto` unless the input ends with a tool's result, which a model answers in words. */
784
+ function responseToolCall(args: ResponsesArgs): ChatToolCall | null {
785
+ const choice = args.echo.tool_choice;
786
+ const named = choice && typeof choice === 'object' ? String((choice as { name?: unknown }).name ?? '') : undefined;
787
+ const answering = args.inputItems.at(-1)?.type === 'function_call_output';
788
+ const functions = (Array.isArray(args.tools) ? args.tools : []).filter((t) => (t as { type?: unknown }).type === 'function');
789
+ if (!functions.length || choice === 'none' || (answering && !named && choice !== 'required')) return null;
790
+ return stubToolCall(functions, 1, named, lastUserText(args.messages));
791
+ }
792
+
793
+ export function buildResponse(args: ResponsesArgs, occurredAt?: string, idSuffix?: string, decision?: ScenarioDecision): OpenAIResponse {
794
+ // The scenario decides the turn exactly as it does for chat completions: a fired handler scripts the
795
+ // text and the tool calls; a miss answers the labeled stub and teaches. The request handler has
796
+ // already honored a fault; only a content decision reaches here.
710
797
  const suffix = idSuffix ?? String(nowEpoch(occurredAt));
798
+ const turn = decision && scenarioTurn(decision, `call-twin-${suffix}`);
799
+ const scripted = turn?.scripted;
800
+ let text = scripted ? (scripted.text ?? '') : stubAssistantText(args.messages, args.model) + (turn?.missTeach ?? '');
801
+ const inputTokens = countPromptTokens(args.messages);
711
802
  const messageItem: ResponseMessageItem = {
712
803
  type: 'message',
713
804
  id: `msg-twin-${suffix}`,
714
805
  status: 'completed',
715
806
  role: 'assistant',
716
- content: [{ type: 'output_text', text, annotations: [] }],
807
+ // no log probabilities unless asked for (`include: ["message.output_text.logprobs"]`): an empty list
808
+ content: [{ type: 'output_text', text, annotations: [], logprobs: [] }],
717
809
  };
810
+ // unscripted, the stub calls a function tool as the chat stub does (buildChoice)
811
+ const stubbed = scripted ? null : responseToolCall(args);
812
+ if (stubbed) text = '';
813
+ const calls: ResponseFunctionCallItem[] = (scripted?.toolCalls ?? (stubbed ? [stubbed] : [])).map((tc, i) => ({ type: 'function_call', id: `fc-twin-${suffix}-${i}`, status: 'completed', call_id: tc.id, name: tc.function.name, arguments: tc.function.arguments }));
814
+ const messageTokens = estimateTokens(text) + estimateTokens(calls.map((c) => c.arguments).join(''));
718
815
  const output: ResponseOutputItem[] = [];
719
- const usage: ResponseUsage = { input_tokens: inputTokens, output_tokens: messageTokens, total_tokens: inputTokens + messageTokens };
816
+ // usage breaks its counts down as OpenAI's does (https://platform.openai.com/docs/api-reference/responses/object, `usage`):
817
+ // the twin caches nothing, so no input token was read from or written to a cache
818
+ const usage: ResponseUsage = { input_tokens: inputTokens, input_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 }, output_tokens: messageTokens, output_tokens_details: { reasoning_tokens: 0 }, total_tokens: inputTokens + messageTokens };
720
819
  // reasoning.effort → a faithful reasoning item (labeled-stub summary) BEFORE the message item,
721
820
  // plus output_tokens_details.reasoning_tokens in usage (counted into output_tokens, like the vendor).
722
821
  if (args.reasoningEffort) {
723
822
  const reasoningTokens = REASONING_BUDGET[args.reasoningEffort] ?? 16;
823
+ // a summary only when one was asked for (`reasoning.summary`); without, OpenAI answers the item's summary empty
824
+ // (https://platform.openai.com/docs/guides/reasoning#reasoning-summaries)
724
825
  const reasoningItem: ResponseReasoningItem = {
725
826
  type: 'reasoning',
726
827
  id: `rs-twin-${suffix}`,
727
- summary: [{ type: 'summary_text', text: `[twin-stub] reasoning summary (effort=${args.reasoningEffort}); the twin cannot run the model, so the chain-of-thought is not real.` }],
828
+ summary: args.reasoningSummary ? [{ type: 'summary_text', text: `[twin-stub] reasoning summary (effort=${args.reasoningEffort}); the twin cannot run the model, so the chain-of-thought is not real.` }] : [],
728
829
  };
729
- output.push(reasoningItem);
830
+ // only a model that reasons lists its reasoning item (openai-models.ts)
831
+ if (reasons(args.model)) output.push(reasoningItem);
730
832
  usage.output_tokens += reasoningTokens;
731
833
  usage.total_tokens += reasoningTokens;
732
834
  usage.output_tokens_details = { reasoning_tokens: reasoningTokens };
733
835
  }
734
- output.push(messageItem);
836
+ // the built-in tools the request offers, which OpenAI runs itself before answering: the placeholder model searches
837
+ // with each one offered, on the input's text, unless `tool_choice` is `none` (the reference's Web search and File search
838
+ // examples answer a `web_search_call` / `file_search_call` item before the message:
839
+ // https://platform.openai.com/docs/guides/tools-web-search, https://platform.openai.com/docs/guides/tools-file-search)
840
+ if (!scripted && args.echo.tool_choice !== 'none') output.push(...builtInCalls(args, suffix));
841
+ // A scripted turn with tool calls and no words carries the calls alone, as the vendor does.
842
+ if (text || !calls.length) output.push(messageItem);
843
+ output.push(...calls);
735
844
  return {
736
845
  id: `resp-twin-${suffix}`,
737
846
  object: 'response',
738
847
  created_at: nowEpoch(occurredAt),
739
848
  status: 'completed',
849
+ // answered whole at once: completed the instant it was created
850
+ completed_at: nowEpoch(occurredAt),
740
851
  model: args.model,
852
+ // no `output_text`: the spec's is an "SDK-only convenience property" the SDKs compute from `output`
741
853
  output,
742
- output_text: text,
743
854
  usage,
744
- ...(args.reasoningEffort ? { reasoning: { effort: args.reasoningEffort, summary: null } } : {}),
745
- ...(args.previousResponseId ? { previous_response_id: args.previousResponseId } : {}),
855
+ ...responseSettings(args),
746
856
  };
747
857
  }
748
858
 
749
- // A deterministic id suffix for a stored response (hash of model + input + chain) so retrieval
750
- // is stable + assertable, like the twin's other deterministic outputs.
751
- function responseSuffix(args: ResponsesArgs): string {
859
+ // Unstored responses have no state to overwrite. Stored identities are allocated atomically below.
860
+ export function responseSuffix(args: ResponsesArgs): string {
752
861
  let h = 0x811c9dc5;
753
- const s = args.model + '|' + args.inputText + '|' + (args.previousResponseId ?? '');
862
+ const s = JSON.stringify([args.model, args.inputItems ?? args.messages, args.previousResponseId, args.instructions]);
754
863
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
755
864
  return (h >>> 0).toString(36);
756
865
  }
757
866
 
758
- // Persist a stored Response so it can be retrieved/deleted later (Responses server-side state).
759
- async function storeResponse(resp: OpenAIResponse, args: ResponsesArgs, req: OpenAIRequest): Promise<void> {
760
- const inputItems = args.messages.map((m, i) => ({ id: `msg-in-${i}`, type: 'message', role: m.role, content: [{ type: 'input_text', text: typeof m.content === 'string' ? m.content : '' }] }));
761
- await applyTwinWrite(SERVICE, {
762
- operation: 'response.create',
763
- subjectType: 'response',
764
- subjectId: resp.id,
765
- fields: { ...resp, _stored: true, _input_items: inputItems },
766
- ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}),
767
- actor: { kind: 'agent' },
867
+ // Allocate and create under the kernel's cross-process lock. Repeated requests are occurrences;
868
+ // a content hash alone would overwrite an earlier response (and its queued scenario decision).
869
+ export async function createStoredResponse(args: ResponsesArgs, req: OpenAIRequest, decision?: ScenarioDecision): Promise<OpenAIResponse | Record<string, unknown>> {
870
+ const { value } = await applyTwinWriteAtomic<OpenAIResponse | Record<string, unknown>>(SERVICE, (resources) => {
871
+ let ordinal = 0;
872
+ for (const row of resources) {
873
+ if (row.type !== 'response') continue;
874
+ const match = /^resp-twin-(\d+)$/.exec(String(row.id));
875
+ if (match) ordinal = Math.max(ordinal, Number(match[1]));
876
+ }
877
+ const suffix = String(ordinal + 1);
878
+ const inputItems = args.inputItems.map((item, i) => ({ ...item, id: item.id ?? `item-in-${suffix}-${i}` }));
879
+ const resp = args.background ? {
880
+ id: `resp-twin-${suffix}`, object: 'response', created_at: nowEpoch(req.occurredAt),
881
+ status: 'queued', completed_at: null, model: args.model, output: [],
882
+ usage: null, ...responseSettings(args),
883
+ } : buildResponse(args, req.occurredAt, suffix, decision);
884
+ return { kind: 'write', value: resp, write: {
885
+ operation: 'response.create', subjectType: 'response', subjectId: resp.id,
886
+ fields: { ...resp, _stored: true, _input_items: inputItems,
887
+ ...(args.background ? { _bg_args: JSON.stringify(args), _bg_suffix: suffix, _bg_decision: decision ? JSON.stringify(decision) : null } : {}),
888
+ },
889
+ ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' },
890
+ } };
768
891
  }, req.root);
769
- }
770
- function responseView(r: Record<string, unknown>): Record<string, unknown> {
771
- return { id: r.id, ...strip(r) };
772
- }
773
-
774
- // ── background responses (background:true → queued → poll → completed | cancel) ───────────
775
- // Persist a `queued` background response: the faithful queued envelope (no output yet) plus the
776
- // stashed args so the first poll can compute the real stub output. background implies store.
777
- async function storeQueuedResponse(args: ResponsesArgs, req: OpenAIRequest, suffix: string): Promise<Record<string, unknown>> {
778
- const id = `resp-twin-${suffix}`;
779
- const inputItems = args.messages.map((m, i) => ({ id: `msg-in-${i}`, type: 'message', role: m.role, content: [{ type: 'input_text', text: typeof m.content === 'string' ? m.content : '' }] }));
780
- const fields = {
781
- id,
782
- object: 'response',
783
- created_at: nowEpoch(req.occurredAt),
784
- status: 'queued',
785
- background: true,
786
- model: args.model,
787
- output: [],
788
- output_text: null,
789
- usage: null,
790
- error: null,
791
- incomplete_details: null,
792
- ...(args.previousResponseId ? { previous_response_id: args.previousResponseId } : {}),
793
- _stored: true,
794
- _input_items: inputItems,
795
- _bg_args: JSON.stringify(args),
796
- _bg_suffix: suffix,
797
- };
798
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'response.create', subjectType: 'response', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
799
- return resource;
800
- }
801
- // First poll of a queued background response: compute the real stub output + usage, flip the
802
- // persisted row to `completed`, and record the (now billable) usage exactly once.
803
- async function completeQueuedResponse(r: Record<string, unknown>, req: OpenAIRequest): Promise<Record<string, unknown>> {
804
- const args = JSON.parse(String(r._bg_args ?? '{}')) as ResponsesArgs;
805
- const suffix = String(r._bg_suffix ?? responseSuffix(args));
806
- const resp = buildResponse(args, req.occurredAt, suffix);
807
- const fields = { ...resp, background: true, status: 'completed', _bg_args: undefined };
808
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'response.update', subjectType: 'response', subjectId: String(r.id), fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
809
- await recordUsage(req, 'responses', resp.model, resp.usage.input_tokens, resp.usage.output_tokens);
810
- return resource;
892
+ return value;
811
893
  }
812
894
 
813
- /** Emit the faithful Responses API streaming events (response.created → output_text.delta* →
814
- * response.completed → [DONE]) into the injected sink. */
815
- export function streamResponse(args: ResponsesArgs, sink: SseSink, occurredAt?: string, idSuffix?: string): OpenAIResponse {
816
- const resp = buildResponse(args, occurredAt, idSuffix);
817
- sink({ data: { type: 'response.created', response: { ...resp, output: [], output_text: '' } } });
818
- // Emit each output item in order; the message item carries the text deltas at its own index.
819
- const msgIndex = resp.output.findIndex((o) => o.type === 'message');
895
+ export function emitResponse(resp: OpenAIResponse, sink: SseSink): OpenAIResponse {
896
+ sink({ data: { type: 'response.created', response: { ...resp, output: [] } } });
897
+ // Emit each output item in order, as the vendor streams them: a reasoning item whole, the message
898
+ // item's text as deltas at its own index, a function call's arguments as one delta then done.
820
899
  resp.output.forEach((item, i) => {
821
- sink({ data: { type: 'response.output_item.added', output_index: i, item } });
822
- if (item.type === 'reasoning') sink({ data: { type: 'response.output_item.done', output_index: i, item } });
900
+ sink({ data: { type: 'response.output_item.added', output_index: i, item: item.type === 'function_call' ? { ...item, arguments: '' } : item } });
901
+ if (item.type === 'message') {
902
+ const text = item.content[0]?.text ?? '';
903
+ for (const piece of chunkText(text)) sink({ data: { type: 'response.output_text.delta', output_index: i, content_index: 0, delta: piece } });
904
+ sink({ data: { type: 'response.output_text.done', output_index: i, content_index: 0, text } });
905
+ }
906
+ if (item.type === 'function_call') {
907
+ sink({ data: { type: 'response.function_call_arguments.delta', output_index: i, item_id: item.id, delta: item.arguments } });
908
+ sink({ data: { type: 'response.function_call_arguments.done', output_index: i, item_id: item.id, arguments: item.arguments } });
909
+ }
910
+ sink({ data: { type: 'response.output_item.done', output_index: i, item } });
823
911
  });
824
- const text = resp.output_text ?? '';
825
- for (const piece of chunkText(text)) {
826
- sink({ data: { type: 'response.output_text.delta', output_index: msgIndex, content_index: 0, delta: piece } });
827
- }
828
- sink({ data: { type: 'response.output_text.done', output_index: msgIndex, content_index: 0, text } });
829
912
  sink({ data: { type: 'response.completed', response: resp } });
830
913
  sink({ done: true });
831
914
  return resp;
832
915
  }
833
916
 
834
917
  // ── Embeddings (deterministic pseudo-vectors) ───────────────────────────────────────────
835
- function handleEmbeddings(params: Record<string, unknown>): OpenAIResponseEnvelope {
918
+ export function handleEmbeddings(params: Record<string, unknown>): OpenAIResponseEnvelope {
836
919
  if (params.model === undefined || params.model === '') return invalidRequest("you must provide a model parameter", 'model');
837
920
  if (params.input === undefined) return invalidRequest("you must provide an input parameter", 'input');
838
921
  const model = String(params.model);
@@ -872,1229 +955,241 @@ function floatsToBase64(vec: number[]): string {
872
955
  return btoa(bin);
873
956
  }
874
957
 
875
- // ── Files (stateful) ────────────────────────────────────────────────────────────────────
876
- async function createFile(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
877
- // The SDK sends multipart/form-data; the twin's JSON contract accepts { purpose, filename,
878
- // bytes } (the server adapts multipart → this shape). purpose is required like the vendor.
879
- const purpose = params.purpose;
880
- if (purpose === undefined || purpose === '') return invalidRequest("you must provide a purpose parameter", 'purpose');
881
- const id = nextId('file', 'file', req.root);
882
- const filename = typeof params.filename === 'string' && params.filename ? params.filename : 'upload.jsonl';
883
- const bytes = Number(params.bytes ?? (typeof params.content === 'string' ? params.content.length : 0));
884
- const fields = {
885
- object: 'file',
886
- bytes: Number.isFinite(bytes) ? bytes : 0,
887
- created_at: nowEpoch(req.occurredAt),
888
- filename,
889
- purpose: String(purpose),
890
- status: 'processed',
891
- // The raw bytes are carved out (openai.files.real_bytes); we keep the supplied content for
892
- // the /content endpoint when present so round-trips are faithful.
893
- _content: typeof params.content === 'string' ? params.content : '',
894
- };
895
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'file.create', subjectType: 'file', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
896
- return { status: 200, body: fileView(resource) };
897
- }
898
- function fileView(r: Record<string, unknown>): Record<string, unknown> {
899
- const { _content: _c, ...rest } = strip(r);
900
- return { id: r.id, ...rest };
901
- }
902
-
903
- // ── Batches (stateful) ──────────────────────────────────────────────────────────────────
904
- async function createBatch(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
905
- if (params.input_file_id === undefined || params.input_file_id === '') return invalidRequest("you must provide an input_file_id parameter", 'input_file_id');
906
- if (params.endpoint === undefined || params.endpoint === '') return invalidRequest("you must provide an endpoint parameter", 'endpoint');
907
- if (params.completion_window === undefined) return invalidRequest("you must provide a completion_window parameter", 'completion_window');
908
- const id = nextId('batch', 'batch', req.root);
909
- const created = nowEpoch(req.occurredAt);
910
- // The twin has nothing to process asynchronously, so a batch completes immediately. The
911
- // results file id is a deterministic synthetic id.
912
- const fields = {
913
- object: 'batch',
914
- endpoint: String(params.endpoint),
915
- input_file_id: String(params.input_file_id),
916
- completion_window: String(params.completion_window),
917
- status: 'completed',
918
- output_file_id: `file-twin-batchout-${id}`,
919
- error_file_id: null,
920
- created_at: created,
921
- in_progress_at: created,
922
- completed_at: created,
923
- request_counts: { total: 0, completed: 0, failed: 0 },
924
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : null,
925
- };
926
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'batch.create', subjectType: 'batch', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
927
- return { status: 200, body: batchView(resource) };
928
- }
929
- function batchView(r: Record<string, unknown>): Record<string, unknown> {
930
- return { id: r.id, ...strip(r) };
931
- }
932
-
933
958
  // ── Moderations (deterministic) ─────────────────────────────────────────────────────────
934
- function handleModerations(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
959
+ export function handleModerations(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
935
960
  if (params.input === undefined) return invalidRequest("you must provide an input parameter", 'input');
936
- const inputs: string[] = typeof params.input === 'string'
937
- ? [params.input]
938
- : Array.isArray(params.input)
939
- ? (params.input as unknown[]).map((x) => (typeof x === 'string' ? x : JSON.stringify(x)))
940
- : [];
961
+ // an array of strings is several inputs, one result each; an array of text and image parts is ONE multimodal input,
962
+ // one result (https://platform.openai.com/docs/api-reference/moderations/create, `input`)
963
+ const list = Array.isArray(params.input) ? (params.input as unknown[]) : [];
964
+ const parts = list.length > 0 && list.every((x) => x && typeof x === 'object' && !Array.isArray(x));
965
+ const inputs: Array<{ text: string; image: boolean }> = typeof params.input === 'string'
966
+ ? [{ text: params.input, image: false }]
967
+ : parts
968
+ ? [{ text: (list as Array<{ type?: unknown; text?: unknown }>).filter((x) => x.type === 'text').map((x) => String(x.text ?? '')).join('\n'), image: (list as Array<{ type?: unknown }>).some((x) => x.type === 'image_url') }]
969
+ : list.map((x) => ({ text: typeof x === 'string' ? x : JSON.stringify(x), image: false }));
941
970
  if (inputs.length === 0) return invalidRequest("'input' must be a non-empty string or array", 'input');
942
- const model = typeof params.model === 'string' && params.model ? params.model : 'omni-moderation-latest';
943
- const results = inputs.map((t) => moderateText(t));
971
+ const model = modelOf('createModeration', params)!;
972
+ const results = inputs.map((t) => moderateText(t.text, t.image));
944
973
  return { status: 200, body: { id: `modr-twin-${nowEpoch(occurredAt)}`, model, results } };
945
974
  }
946
975
 
947
- // ── Fine-tuning jobs (stateful) ─────────────────────────────────────────────────────────
948
- async function createFineTune(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
949
- if (params.model === undefined || params.model === '') return invalidRequest("you must provide a model parameter", 'model');
950
- if (params.training_file === undefined || params.training_file === '') return invalidRequest("you must provide a training_file parameter", 'training_file');
951
- const id = nextId('ftjob', 'ftjob', req.root);
952
- const created = nowEpoch(req.occurredAt);
953
- // The twin cannot train a model, so the job completes immediately with a synthetic
954
- // fine-tuned model id (the lifecycle/shape is faithful; the trained model is a stub).
955
- const fields = {
956
- object: 'fine_tuning.job',
957
- model: String(params.model),
958
- created_at: created,
959
- finished_at: created,
960
- fine_tuned_model: `ft:${String(params.model)}:twin::${id}`,
961
- organization_id: 'org-twin',
962
- status: 'succeeded',
963
- training_file: String(params.training_file),
964
- validation_file: params.validation_file ?? null,
965
- hyperparameters: (params.hyperparameters && typeof params.hyperparameters === 'object') ? params.hyperparameters : { n_epochs: 'auto' },
966
- result_files: [],
967
- trained_tokens: 0,
968
- error: null,
969
- seed: Number(params.seed ?? 0),
970
- };
971
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'fine_tuning_job.create', subjectType: 'fine_tuning_job', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
972
- return { status: 200, body: ftView(resource) };
973
- }
974
- function ftView(r: Record<string, unknown>): Record<string, unknown> {
975
- return { id: r.id, ...strip(r) };
976
- }
977
- /** The fine-tuned model objects minted by succeeded fine-tuning jobs (deletable, owned by the
978
- * user), minus any that have been deleted via DELETE /v1/models/:id. */
979
- function fineTunedModels(root?: string): Array<{ id: string; object: 'model'; created: number; owned_by: string }> {
980
- const deleted = new Set(rows('model', root).filter((r) => r._deleted).map((r) => String(r.id)));
981
- const out: Array<{ id: string; object: 'model'; created: number; owned_by: string }> = [];
982
- for (const j of rows('fine_tuning_job', root)) {
983
- const ftm = (j as { fine_tuned_model?: unknown }).fine_tuned_model;
984
- if (typeof ftm === 'string' && ftm && !deleted.has(ftm)) {
985
- out.push({ id: ftm, object: 'model', created: Number(j.created_at ?? 0), owned_by: 'org-twin' });
986
- }
987
- }
988
- return out;
989
- }
990
-
991
- // ── Vector stores (stateful) ────────────────────────────────────────────────────────────
992
- async function createVectorStore(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
993
- const id = nextId('vector_store', 'vs', req.root);
994
- const created = nowEpoch(req.occurredAt);
995
- const fileIds = Array.isArray(params.file_ids) ? (params.file_ids as unknown[]) : [];
996
- const fields = {
997
- object: 'vector_store',
998
- created_at: created,
999
- name: typeof params.name === 'string' ? params.name : null,
1000
- usage_bytes: 0,
1001
- status: 'completed',
1002
- file_counts: { in_progress: 0, completed: fileIds.length, failed: 0, cancelled: 0, total: fileIds.length },
1003
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1004
- last_active_at: created,
1005
- };
1006
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'vector_store.create', subjectType: 'vector_store', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1007
- // Seed any provided file_ids as vector_store_file children.
1008
- for (const fid of fileIds) {
1009
- await addVectorStoreFile(id, String(fid), req);
1010
- }
1011
- return { status: 200, body: vsView(resource) };
1012
- }
1013
- function vsView(r: Record<string, unknown>): Record<string, unknown> {
1014
- return { id: r.id, ...strip(r) };
1015
- }
1016
- async function addVectorStoreFile(storeId: string, fileId: string, req: OpenAIRequest, batchId?: string): Promise<Record<string, unknown>> {
1017
- const created = nowEpoch(req.occurredAt);
1018
- const childId = `${storeId}::${fileId}`;
1019
- const fields = { object: 'vector_store.file', vector_store_id: storeId, file_id: fileId, created_at: created, status: 'completed', usage_bytes: 0, ...(batchId ? { _batch_id: batchId } : {}) };
1020
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'vector_store_file.create', subjectType: 'vector_store_file', subjectId: childId, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1021
- return vsFileView(resource);
1022
- }
1023
- function vsFileView(r: Record<string, unknown>): Record<string, unknown> {
1024
- const s = strip(r);
1025
- return { id: String(r.id).split('::')[1] ?? r.id, object: 'vector_store.file', file_id: s.file_id, vector_store_id: s.vector_store_id, created_at: s.created_at, status: s.status, usage_bytes: s.usage_bytes ?? 0 };
1026
- }
1027
- function vsBatchView(r: Record<string, unknown>): Record<string, unknown> {
1028
- const { _file_ids: _f, ...rest } = strip(r);
1029
- return { id: r.id, ...rest };
1030
- }
976
+ // ── Images (a real placeholder image, no model: ./openai-media.ts) ─────────────────────────────────
977
+ const gptImage = (model: string): boolean => /^(gpt-image|chatgpt-image)/.test(model);
1031
978
 
1032
- // ── Admin / org API (stateful) ──────────────────────────────────────────────────────────
1033
- // The Admin API (under /v1/organization, authed with an admin key) manages organization
1034
- // projects and their API keys. The twin models projects (create/list/retrieve/modify/archive)
1035
- // and project API keys (create/list/retrieve/delete) statefully. Keys are SYNTHETIC twin keys —
1036
- // never real OpenAI credentials.
1037
- async function createProject(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1038
- if (params.name === undefined || params.name === '') return invalidRequest("you must provide a name parameter", 'name');
1039
- const id = nextId('project', 'proj', req.root);
1040
- const created = nowEpoch(req.occurredAt);
1041
- const fields = { object: 'organization.project', name: String(params.name), created_at: created, archived_at: null, status: 'active' };
1042
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'project.create', subjectType: 'project', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1043
- return { status: 200, body: idView(resource) };
979
+ /** One answered image: the GPT image models give the image itself as base64, always; `dall-e-2` and `dall-e-3` a URL
980
+ * unless `response_format` is `b64_json`, and `dall-e-3` its revised prompt (developers.openai.com/api/reference/
981
+ * resources/images/methods/generate, `response_format`; the Image object's `b64_json`, `url`, `revised_prompt`). */
982
+ function imageData(params: Record<string, unknown>, model: string, size: { width: number; height: number }, seed: string, kind: string, i: number, occurredAt?: string): Record<string, unknown> {
983
+ return gptImage(model) || params.response_format === 'b64_json' ? { b64_json: base64(placeholderPng(size, `${seed} #${i + 1}`)) } : dalleUrl(model, seed, kind, i, occurredAt);
1044
984
  }
1045
- async function createProjectApiKey(projectId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1046
- if (params.name === undefined || params.name === '') return invalidRequest("you must provide a name parameter", 'name');
1047
- const seq = rows('api_key', req.root).filter((r) => r.project_id === projectId).length + 1;
1048
- const id = `key_twin_${projectId}_${seq}`;
1049
- const created = nowEpoch(req.occurredAt);
1050
- // a SYNTHETIC twin secret — clearly not a real OpenAI key (only shown at creation, like the vendor).
1051
- const secret = `sk-twin-proj-${projectId}-${seq}`;
1052
- const fields = { object: 'organization.project.api_key', name: String(params.name), created_at: created, project_id: projectId, redacted_value: `sk-twin-...${seq}`, _secret: secret };
1053
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'api_key.create', subjectType: 'api_key', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1054
- // creation echoes the one-time secret `value`; subsequent reads only show redacted_value.
1055
- return { status: 200, body: { ...apiKeyView(resource), value: secret } };
985
+ /** A DALL·E image as its URL, the default for `dall-e-2` and `dall-e-3`. */
986
+ function dalleUrl(model: string, seed: string, kind: string, i: number, occurredAt?: string): Record<string, unknown> {
987
+ return { url: `https://twin.invalid/openai-${kind}-stub/${nowEpoch(occurredAt)}-${i}.png`, ...(model === 'dall-e-3' ? { revised_prompt: `[twin-stub] ${seed}` } : {}) };
1056
988
  }
1057
- function apiKeyView(r: Record<string, unknown>): Record<string, unknown> {
1058
- const { _secret: _s, ...rest } = strip(r);
1059
- return { id: r.id, ...rest };
1060
- }
1061
-
1062
- // ── Uploads API (multipart large-file parts, stateful) ──────────────────────────────────
1063
- // The Uploads API lets a client stream a large file in parts: create an upload, POST each part,
1064
- // then complete (which assembles the parts into a real File object). The twin models the full
1065
- // lifecycle and assembles the concatenated part contents into a File (faithful, stateful).
1066
- async function createUpload(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1067
- for (const k of ['filename', 'purpose', 'bytes', 'mime_type']) {
1068
- if (params[k] === undefined || params[k] === '') return invalidRequest(`you must provide a ${k} parameter`, k);
1069
- }
1070
- const id = nextId('upload', 'upload', req.root);
1071
- const created = nowEpoch(req.occurredAt);
1072
- const fields = {
1073
- object: 'upload', created_at: created, filename: String(params.filename), bytes: Number(params.bytes),
1074
- purpose: String(params.purpose), mime_type: String(params.mime_type), status: 'pending',
1075
- expires_at: created + 3600, file: null,
1076
- };
1077
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'upload.create', subjectType: 'upload', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1078
- return { status: 200, body: uploadView(resource) };
1079
- }
1080
- function uploadView(r: Record<string, unknown>): Record<string, unknown> {
1081
- const { _parts: _p, ...rest } = strip(r);
1082
- return { id: r.id, ...rest };
1083
- }
1084
- async function addUploadPart(uploadId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1085
- const up = getRow('upload', uploadId, req.root);
1086
- if (!up || up.status !== 'pending') return notFound(`No such Upload object: ${uploadId}`);
1087
- if (params.data === undefined) return invalidRequest("you must provide a data parameter", 'data');
1088
- const parts = Array.isArray((up as { _parts?: unknown })._parts) ? [...((up as { _parts: unknown[] })._parts)] : [];
1089
- const partId = `part-twin-${uploadId}-${parts.length + 1}`;
1090
- parts.push({ id: partId, data: String(params.data) });
1091
- await applyTwinWrite(SERVICE, { operation: 'upload.update', subjectType: 'upload', subjectId: uploadId, fields: { _parts: parts }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1092
- return { status: 200, body: { id: partId, object: 'upload.part', created_at: nowEpoch(req.occurredAt), upload_id: uploadId } };
1093
- }
1094
- async function completeUpload(uploadId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1095
- const up = getRow('upload', uploadId, req.root);
1096
- if (!up || up.status !== 'pending') return notFound(`No such Upload object: ${uploadId}`);
1097
- if (!Array.isArray(params.part_ids) || params.part_ids.length === 0) return invalidRequest("you must provide a part_ids array", 'part_ids');
1098
- const parts = Array.isArray((up as { _parts?: unknown })._parts) ? (up as { _parts: Array<{ id: string; data: string }> })._parts : [];
1099
- const byId = new Map(parts.map((p) => [p.id, p.data]));
1100
- // assemble the file content from the parts in the caller-specified order.
1101
- let content = '';
1102
- for (const pid of params.part_ids as string[]) {
1103
- if (!byId.has(String(pid))) return invalidRequest(`unknown part_id '${pid}'`, 'part_ids');
1104
- content += byId.get(String(pid));
1105
- }
1106
- // mint a real File object from the assembled content.
1107
- const fileId = nextId('file', 'file', req.root);
1108
- const created = nowEpoch(req.occurredAt);
1109
- const fileFields = {
1110
- object: 'file', bytes: Number(up.bytes ?? content.length), created_at: created,
1111
- filename: String(up.filename), purpose: String(up.purpose), status: 'processed', _content: content,
1112
- };
1113
- await applyTwinWrite(SERVICE, { operation: 'file.create', subjectType: 'file', subjectId: fileId, fields: fileFields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1114
- const fileObj = fileView({ id: fileId, ...fileFields });
1115
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'upload.update', subjectType: 'upload', subjectId: uploadId, fields: { status: 'completed', file: fileObj }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1116
- return { status: 200, body: uploadView(resource) };
1117
- }
1118
-
1119
- // ── Assistants / Threads / Runs (beta, stateful) ────────────────────────────────────────
1120
- // The Assistants API is fully stateful CRUD over assistants, threads (+ messages), and runs
1121
- // (+ run steps). The twin models the OBJECTS + lifecycle faithfully; a run cannot invoke a real
1122
- // model, so a run completes immediately and any assistant reply message is a clearly-labeled stub.
1123
- async function createAssistant(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1124
- if (params.model === undefined || params.model === '') return invalidRequest("you must provide a model parameter", 'model');
1125
- const id = nextId('assistant', 'asst', req.root);
1126
- const created = nowEpoch(req.occurredAt);
1127
- const fields = {
1128
- object: 'assistant', created_at: created,
1129
- name: params.name ?? null, description: params.description ?? null,
1130
- model: String(params.model), instructions: params.instructions ?? null,
1131
- tools: Array.isArray(params.tools) ? params.tools : [],
1132
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1133
- temperature: params.temperature ?? null, top_p: params.top_p ?? null,
1134
- response_format: params.response_format ?? 'auto',
1135
- };
1136
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'assistant.create', subjectType: 'assistant', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1137
- return { status: 200, body: idView(resource) };
1138
- }
1139
- function idView(r: Record<string, unknown>): Record<string, unknown> {
1140
- return { id: r.id, ...strip(r) };
1141
- }
1142
- /** Pick the updatable fields present in an update body (vendor PATCH-via-POST semantics). */
1143
- function pickUpdate(params: Record<string, unknown>, keys: string[]): Record<string, unknown> {
1144
- const out: Record<string, unknown> = {};
1145
- for (const k of keys) if (params[k] !== undefined) out[k] = params[k];
1146
- return out;
1147
- }
1148
- async function createThread(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1149
- const id = nextId('thread', 'thread', req.root);
1150
- const created = nowEpoch(req.occurredAt);
1151
- const fields = { object: 'thread', created_at: created, metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {}, tool_resources: params.tool_resources ?? null };
1152
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'thread.create', subjectType: 'thread', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1153
- // Seed any inline messages provided at thread creation.
1154
- if (Array.isArray(params.messages)) {
1155
- for (const m of params.messages as Array<Record<string, unknown>>) await addThreadMessage(id, m, req);
1156
- }
1157
- return { status: 200, body: idView(resource) };
1158
- }
1159
- async function addThreadMessage(threadId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<Record<string, unknown>> {
1160
- const seq = rows('message', req.root).filter((r) => r.thread_id === threadId).length + 1;
1161
- const id = `msg-twin-${threadId}-${seq}`;
1162
- const created = nowEpoch(req.occurredAt);
1163
- const role = typeof params.role === 'string' ? params.role : 'user';
1164
- const text = typeof params.content === 'string' ? params.content : Array.isArray(params.content) ? (params.content as Array<Record<string, unknown>>).map((p) => String((p as { text?: { value?: unknown } }).text?.value ?? (p as { text?: unknown }).text ?? '')).join('') : '';
1165
- const fields = {
1166
- object: 'thread.message', created_at: created, thread_id: threadId, role,
1167
- content: [{ type: 'text', text: { value: text, annotations: [] } }],
1168
- assistant_id: params.assistant_id ?? null, run_id: params.run_id ?? null,
1169
- attachments: Array.isArray(params.attachments) ? params.attachments : [],
1170
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1171
- _seq: seq,
1172
- };
1173
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'message.create', subjectType: 'message', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1174
- return msgView(resource);
1175
- }
1176
- function msgView(r: Record<string, unknown>): Record<string, unknown> {
1177
- const { _seq: _s, ...rest } = strip(r);
1178
- return { id: r.id, ...rest };
1179
- }
1180
- // A run completes immediately (the twin can't invoke the model): it appends a clearly-labeled
1181
- // stub assistant message to the thread and records two run steps (message creation lifecycle).
1182
- async function createRun(threadId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1183
- if (params.assistant_id === undefined || params.assistant_id === '') return invalidRequest("you must provide an assistant_id parameter", 'assistant_id');
1184
- const assistantId = String(params.assistant_id);
1185
- const assistant = getRow('assistant', assistantId, req.root);
1186
- if (!assistant || assistant._deleted) return notFound(`No assistant found with id '${assistantId}'.`);
1187
- const id = nextId('run', 'run', req.root);
1188
- const created = nowEpoch(req.occurredAt);
1189
- const model = typeof params.model === 'string' && params.model ? params.model : String(assistant.model);
1190
- // append the stub assistant reply to the thread.
1191
- const replyText = `[twin-stub:${model}] deterministic assistant run output (no model weights are run)`;
1192
- const reply = await addThreadMessage(threadId, { role: 'assistant', content: replyText, assistant_id: assistantId, run_id: id }, req);
1193
- const fields = {
1194
- object: 'thread.run', created_at: created, thread_id: threadId, assistant_id: assistantId,
1195
- status: 'completed', model, instructions: params.instructions ?? assistant.instructions ?? null,
1196
- tools: Array.isArray(params.tools) ? params.tools : (assistant.tools ?? []),
1197
- started_at: created, completed_at: created, expires_at: null, cancelled_at: null, failed_at: null,
1198
- required_action: null, last_error: null,
1199
- usage: { prompt_tokens: 0, completion_tokens: estimateTokens(replyText), total_tokens: estimateTokens(replyText) },
1200
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1201
- _reply_msg: reply.id,
989
+ function imageCount(params: Record<string, unknown>): number {
990
+ const n = Number(params.n ?? 1);
991
+ return Number.isInteger(n) && n > 0 ? n : 1;
992
+ }
993
+ /** An image answer, and its events when streamed. */
994
+ export type ImageAnswer = OpenAIResponseEnvelope & { events?: Array<{ event: string; data: unknown }> };
995
+
996
+ /** A GPT image model's answer: the images with the tokens they cost (the image generation guide's 1024x1024
997
+ * medium-quality image is 1056 output tokens, scaled here by area; the twin reads no pixels, so it counts no input image
998
+ * tokens: https://platform.openai.com/docs/guides/image-generation#cost-and-latency), or, with `stream`, the
999
+ * `partial_images` asked (none by default: "When set to 0, the response will be a single image sent in one streaming
1000
+ * event") then the completed image (https://platform.openai.com/docs/api-reference/images-streaming). */
1001
+ function gptImageAnswer(params: Record<string, unknown>, size: { width: number; height: number }, data: Array<Record<string, unknown>>, kind: 'image_generation' | 'image_edit', occurredAt?: string): ImageAnswer {
1002
+ const text = estimateTokens(String(params.prompt ?? ''));
1003
+ const output = Math.ceil((1056 * size.width * size.height) / (1024 * 1024)) * data.length;
1004
+ const usage = { total_tokens: text + output, input_tokens: text, output_tokens: output, input_tokens_details: { text_tokens: text, image_tokens: 0 } };
1005
+ const created = nowEpoch(occurredAt);
1006
+ if (params.stream !== true && params.stream !== 'true') return { status: 200, body: { created, data, usage } };
1007
+ const settings = {
1008
+ created_at: created, size: `${size.width}x${size.height}`,
1009
+ // what `auto` settles on for a placeholder: an opaque PNG of medium quality
1010
+ quality: params.quality && params.quality !== 'auto' ? params.quality : 'medium',
1011
+ background: params.background && params.background !== 'auto' ? params.background : 'opaque',
1012
+ output_format: params.output_format ?? 'png',
1202
1013
  };
1203
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'run.create', subjectType: 'run', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1204
- return { status: 200, body: runView(resource) };
1205
- }
1206
- function runView(r: Record<string, unknown>): Record<string, unknown> {
1207
- const { _reply_msg: _m, ...rest } = strip(r);
1208
- return { id: r.id, ...rest };
1209
- }
1210
- function runSteps(run: Record<string, unknown>): Array<Record<string, unknown>> {
1211
- const created = Number(run.created_at ?? 0);
1212
- const replyMsg = String((run as { _reply_msg?: unknown })._reply_msg ?? '');
1213
- return [{
1214
- id: `step-${run.id}-1`, object: 'thread.run.step', created_at: created,
1215
- run_id: String(run.id), assistant_id: String(run.assistant_id), thread_id: String(run.thread_id),
1216
- type: 'message_creation', status: 'completed', completed_at: created,
1217
- step_details: { type: 'message_creation', message_creation: { message_id: replyMsg } },
1218
- usage: run.usage ?? null,
1219
- }];
1014
+ const partials = Math.min(3, Math.max(0, Number(params.partial_images ?? 0) || 0));
1015
+ const b64 = String(data[0]?.b64_json ?? '');
1016
+ const events = [
1017
+ ...Array.from({ length: partials }, (_, i) => ({ event: `${kind}.partial_image`, data: { type: `${kind}.partial_image`, b64_json: b64, ...settings, partial_image_index: i } })),
1018
+ { event: `${kind}.completed`, data: { type: `${kind}.completed`, b64_json: b64, ...settings, usage } },
1019
+ ];
1020
+ return { status: 200, body: { created, data, usage }, events };
1220
1021
  }
1221
1022
 
1222
- // ── Images (response shape; real pixels out of scope) ───────────────────────────────────
1223
- function handleImages(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
1023
+ export function handleImages(params: Record<string, unknown>, occurredAt?: string): ImageAnswer {
1224
1024
  if (params.prompt === undefined || params.prompt === '') return invalidRequest("you must provide a prompt parameter", 'prompt');
1225
- const n = Number(params.n ?? 1);
1226
- const count = Number.isInteger(n) && n > 0 ? n : 1;
1227
- const data = Array.from({ length: count }, (_, i) => ({
1228
- url: `https://twin.invalid/openai-image-stub/${nowEpoch(occurredAt)}-${i}.png`,
1229
- revised_prompt: `[twin-stub] ${String(params.prompt)}`,
1230
- }));
1025
+ const model = modelOf('createImage', params)!;
1026
+ const size = requestedSize(params.size);
1027
+ const data = Array.from({ length: imageCount(params) }, (_, i) => imageData(params, model, size, String(params.prompt), 'image', i, occurredAt));
1028
+ if (gptImage(model)) return gptImageAnswer(params, size, data, 'image_generation', occurredAt);
1231
1029
  return { status: 200, body: { created: nowEpoch(occurredAt), data } };
1232
1030
  }
1233
- // Image EDITS (require image + prompt) and VARIATIONS (require image, no prompt). Real pixels are
1234
- // out of scope, so the twin returns the faithful response shape with a placeholder URL.
1235
- function handleImageEdit(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
1236
- if (params.image === undefined || params.image === '') return invalidRequest("you must provide an image to edit", 'image');
1031
+ /** OpenAI's refusal of an upload that is not an image the model takes. */
1032
+ const badImage = (formats: string): OpenAIResponseEnvelope => invalidRequest(`Invalid image file: 'image' must be ${formats}.`, 'image', 'invalid_image_format');
1033
+
1034
+ // An EDIT takes an image file and a prompt; a VARIATION (dall-e-2 only) a square PNG. Each is told by its bytes.
1035
+ export function handleImageEdit(params: Record<string, unknown>, occurredAt?: string): ImageAnswer {
1036
+ // a GPT image model edits several images sent as `image[]` (the form keeps the last one named so)
1037
+ const image = params.image ?? params['image[]'];
1038
+ if (image === undefined || image === '') return invalidRequest("you must provide an image to edit", 'image');
1237
1039
  if (params.prompt === undefined || params.prompt === '') return invalidRequest("you must provide a prompt parameter", 'prompt');
1238
- const n = Number(params.n ?? 1);
1239
- const count = Number.isInteger(n) && n > 0 ? n : 1;
1240
- const data = Array.from({ length: count }, (_, i) => ({ url: `https://twin.invalid/openai-image-edit-stub/${nowEpoch(occurredAt)}-${i}.png` }));
1040
+ const model = modelOf('createImageEdit', params)!;
1041
+ const bytes = partBytes(image);
1042
+ const format = bytes && imageFormat(bytes);
1043
+ if (gptImage(model) ? !format : format !== 'png' || !square(bytes!)) return badImage(gptImage(model) ? 'a png, webp, or jpg file' : 'a square png file');
1044
+ const size = requestedSize(params.size, format === 'png' ? pngSize(bytes!) : undefined);
1045
+ const data = Array.from({ length: imageCount(params) }, (_, i) => imageData(params, model, size, String(params.prompt), 'image-edit', i, occurredAt));
1046
+ if (gptImage(model)) return gptImageAnswer(params, size, data, 'image_edit', occurredAt);
1241
1047
  return { status: 200, body: { created: nowEpoch(occurredAt), data } };
1242
1048
  }
1243
- function handleImageVariation(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
1049
+ const square = (png: Uint8Array): boolean => { const s = pngSize(png); return s.width === s.height; };
1050
+ export function handleImageVariation(params: Record<string, unknown>, occurredAt?: string): OpenAIResponseEnvelope {
1244
1051
  if (params.image === undefined || params.image === '') return invalidRequest("you must provide an image", 'image');
1245
- const n = Number(params.n ?? 1);
1246
- const count = Number.isInteger(n) && n > 0 ? n : 1;
1247
- const data = Array.from({ length: count }, (_, i) => ({ url: `https://twin.invalid/openai-image-variation-stub/${nowEpoch(occurredAt)}-${i}.png` }));
1052
+ const bytes = partBytes(params.image);
1053
+ if (!bytes || imageFormat(bytes) !== 'png' || !square(bytes)) return badImage('a valid square png file');
1054
+ const size = requestedSize(params.size);
1055
+ const data = Array.from({ length: imageCount(params) }, (_, i) => imageData(params, modelOf('createImageVariation', params)!, size, 'variation', 'image-variation', i, occurredAt));
1248
1056
  return { status: 200, body: { created: nowEpoch(occurredAt), data } };
1249
1057
  }
1250
1058
 
1251
- // ── Audio (deterministic stubs; real model output is out of scope) ──────────────────────
1059
+ // ── Audio (deterministic stubs; the twin runs no model) ──────────────────────
1252
1060
  // Transcription/translation cannot run a real speech model, so the twin returns a clearly
1253
- // labeled deterministic transcript derived from the supplied filename/text. The response SHAPE
1061
+ // labeled deterministic transcript naming the uploaded file. It takes the audio file itself, in a
1062
+ // format OpenAI transcribes (./openai-media.ts), never a file's name. The response SHAPE
1254
1063
  // (json / verbose_json / text) is vendor-faithful.
1255
- function handleTranscription(params: Record<string, unknown>, translate: boolean): OpenAIResponseEnvelope {
1256
- // The server adapts multipart → JSON { file (filename), model, response_format, language }.
1064
+ /** A multipart list field (`include[]`, `timestamp_granularities[]`), however the form named it. */
1065
+ function formList(params: Record<string, unknown>, name: string): string[] {
1066
+ const v = params[name] ?? params[`${name}[]`];
1067
+ return (Array.isArray(v) ? v : v === undefined ? [] : [v]).map(String);
1068
+ }
1069
+
1070
+ /** What a transcription is billed on: the GPT-4o transcribe models by tokens, whisper-1 and the diarizing model by the
1071
+ * audio's seconds (https://platform.openai.com/docs/api-reference/audio/json-object, `usage`). The twin counts ten
1072
+ * audio tokens a second and its transcript's text tokens; neither is a model's count. */
1073
+ function transcriptionUsage(model: string, seconds: number, text: string): Record<string, unknown> {
1074
+ if (model.startsWith('whisper') || model.includes('diarize')) return { type: 'duration', seconds: Math.ceil(seconds) };
1075
+ const audio = Math.ceil(seconds * 10);
1076
+ const out = estimateTokens(text);
1077
+ return { type: 'tokens', input_tokens: audio, input_token_details: { text_tokens: 0, audio_tokens: audio }, output_tokens: out, total_tokens: audio + out };
1078
+ }
1079
+
1080
+ /** The transcript's words spread evenly over the audio: the twin hears nothing, so its timings are the file's length
1081
+ * shared out, not a model's alignment. */
1082
+ function words(text: string, seconds: number): Array<{ word: string; start: number; end: number }> {
1083
+ const all = text.split(/\s+/).filter(Boolean);
1084
+ const each = seconds / Math.max(1, all.length);
1085
+ return all.map((word, i) => ({ word, start: i * each, end: (i + 1) * each }));
1086
+ }
1087
+
1088
+ export type Transcription = { status: 200; body: unknown; seconds: number; events?: Array<{ data: unknown }> } | OpenAIResponseEnvelope;
1089
+
1090
+ /** A transcription (or translation) of the uploaded audio: a labeled transcript naming the file, in the response
1091
+ * format asked (json, text, verbose_json with the word or segment timestamps asked, or diarized_json), with the
1092
+ * token log probabilities when `include[]=logprobs`, and as `transcript.text.delta` events then
1093
+ * `transcript.text.done` when `stream=true` on a model that streams (https://platform.openai.com/docs/api-reference/audio/createTranscription). */
1094
+ export function handleTranscription(params: Record<string, unknown>, translate: boolean): Transcription {
1257
1095
  if (params.file === undefined || params.file === '') return invalidRequest("you must provide a file parameter", 'file');
1258
1096
  if (params.model === undefined || params.model === '') return invalidRequest("you must provide a model parameter", 'model');
1259
- const filename = String(params.file);
1097
+ const bytes = partBytes(params.file);
1098
+ if (!bytes || !audioFormat(bytes)) return invalidRequest("Invalid file format. Supported formats: ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm']", 'file', 'invalid_value');
1099
+ const filename = String((params.file as { name?: string }).name || 'upload');
1100
+ const model = String(params.model);
1260
1101
  const verb = translate ? 'translation' : 'transcription';
1261
1102
  const text = `[twin-stub] deterministic ${verb} of ${filename} (no speech model is run)`;
1262
1103
  const format = typeof params.response_format === 'string' ? params.response_format : 'json';
1263
- if (format === 'text') return { status: 200, body: text };
1104
+ // the file's own length where the twin can read it (a WAV's header), else a labeled second
1105
+ const duration = wavSeconds(bytes) ?? 1.0;
1106
+ if (format === 'text') return { status: 200, body: text, seconds: duration };
1107
+ if (translate) return { status: 200, seconds: duration, body: format === 'verbose_json' ? { task: 'translation', language: 'english', duration, text, segments: [{ id: 0, start: 0, end: duration, text }] } : { text } };
1108
+ const usage = transcriptionUsage(model, duration, text);
1109
+ if (format === 'diarized_json') {
1110
+ // one speaker the twin cannot tell apart: the first name the caller knows, else OpenAI's first label
1111
+ const speaker = formList(params, 'known_speaker_names')[0] ?? 'A';
1112
+ return { status: 200, seconds: duration, body: { task: 'transcribe', duration, text, segments: [{ type: 'transcript.text.segment', id: 'seg_001', start: 0, end: duration, text, speaker }], usage } };
1113
+ }
1264
1114
  if (format === 'verbose_json') {
1265
- return { status: 200, body: { task: verb, language: translate ? 'english' : (typeof params.language === 'string' ? params.language : 'english'), duration: 1.0, text, segments: [{ id: 0, start: 0, end: 1, text }] } };
1115
+ const granularities = formList(params, 'timestamp_granularities');
1116
+ const segment = { id: 0, seek: 0, start: 0, end: duration, text, tokens: [], temperature: 0, avg_logprob: 0, compression_ratio: 1, no_speech_prob: 0 };
1117
+ return {
1118
+ status: 200,
1119
+ seconds: duration,
1120
+ body: {
1121
+ task: 'transcribe', language: typeof params.language === 'string' ? params.language : 'english', duration, text,
1122
+ ...(granularities.includes('word') ? { words: words(text, duration) } : {}),
1123
+ ...(!granularities.length || granularities.includes('segment') ? { segments: [segment] } : {}),
1124
+ usage,
1125
+ },
1126
+ };
1266
1127
  }
1267
- return { status: 200, body: { text } };
1268
- }
1269
- // Text-to-speech: cannot synthesize real audio (out of scope), but the endpoint returns audio
1270
- // bytes deterministically derived from the input so the round-trip shape is faithful.
1271
- function handleSpeech(params: Record<string, unknown>): OpenAIResponseEnvelope {
1128
+ const logprobs = formList(params, 'include').includes('logprobs') ? buildLogprobs(text, 0).content.map(({ token, logprob, bytes: b }) => ({ token, logprob, bytes: b })) : undefined;
1129
+ const body = { text, ...(logprobs ? { logprobs } : {}), usage };
1130
+ // whisper-1 does not stream ("Streaming is not supported for the whisper-1 model and will be ignored")
1131
+ if (String(params.stream) === 'true' && !model.startsWith('whisper')) {
1132
+ const pieces = text.match(/\s*\S+/g) ?? [];
1133
+ const events = pieces.map((delta) => ({ data: { type: 'transcript.text.delta', delta, ...(logprobs ? { logprobs: buildLogprobs(delta, 0).content.map(({ token, logprob, bytes: b }) => ({ token, logprob, bytes: b })) } : {}) } }));
1134
+ events.push({ data: { type: 'transcript.text.done', text, ...(logprobs ? { logprobs } : {}), usage } as never });
1135
+ return { status: 200, body, seconds: duration, events };
1136
+ }
1137
+ return { status: 200, body, seconds: duration };
1138
+ }
1139
+ // Text-to-speech: runs no speech model, but answers the audio file itself in the requested format, as OpenAI does
1140
+ // (a second of silence: ./openai-media.ts), with that format's Content-Type.
1141
+ const SPEECH: Record<string, { type: string; file: () => Uint8Array }> = {
1142
+ mp3: { type: 'audio/mpeg', file: silentMp3 },
1143
+ opus: { type: 'audio/opus', file: silentOpus },
1144
+ aac: { type: 'audio/aac', file: silentAac },
1145
+ flac: { type: 'audio/flac', file: silentFlac },
1146
+ wav: { type: 'audio/wav', file: silentWav },
1147
+ pcm: { type: 'audio/pcm', file: silentPcm },
1148
+ };
1149
+ export function handleSpeech(params: Record<string, unknown>): OpenAIResponseEnvelope | { status: 200; audio: Uint8Array; type: string } {
1272
1150
  if (params.model === undefined || params.model === '') return invalidRequest("you must provide a model parameter", 'model');
1273
1151
  if (params.input === undefined || params.input === '') return invalidRequest("you must provide an input parameter", 'input');
1274
1152
  if (params.voice === undefined || params.voice === '') return invalidRequest("you must provide a voice parameter", 'voice');
1275
- // Deterministic placeholder "audio" payload (base64 of a labeled stub), never real audio.
1276
- const marker = `[twin-stub-audio] voice=${String(params.voice)} format=${String(params.response_format ?? 'mp3')} text=${String(params.input)}`;
1277
- return { status: 200, body: { object: 'audio.speech', audio_base64: btoa(marker), format: String(params.response_format ?? 'mp3') } };
1278
- }
1279
-
1280
- // ── Vector store search (deterministic ranking over attached files) ──────────────────────
1281
- // The twin can't run a real similarity search (no embeddings of file content), so it returns
1282
- // the attached files ranked by a deterministic pseudo-score seeded from (query, file_id). The
1283
- // response SHAPE (object:vector_store.search_results.page, data[].score/file_id/content) is faithful.
1284
- function searchScore(query: string, fileId: string): number {
1285
- let h = 0x811c9dc5;
1286
- const s = query + '|' + fileId;
1287
- for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
1288
- return (h >>> 0) / 0xffffffff;
1289
- }
1290
-
1291
- // ── Evals API (stateful: eval config → runs → output items) ──────────────────────────────
1292
- // The Evals API systematically evaluates model output. The twin cannot run a real grader, so a
1293
- // run completes immediately with a deterministic synthetic result + one output item per datasource
1294
- // item (passed, since the twin's stub output is graded by a deterministic stub grader). The eval/
1295
- // run/output-item SHAPES are vendor-faithful; the grading values are clearly-deterministic stubs.
1296
- async function createEval(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1297
- if (params.data_source_config === undefined) return invalidRequest("you must provide a data_source_config parameter", 'data_source_config');
1298
- if (params.testing_criteria === undefined || !Array.isArray(params.testing_criteria)) return invalidRequest("you must provide a testing_criteria parameter (array)", 'testing_criteria');
1299
- const id = nextId('eval', 'eval', req.root);
1300
- const fields = {
1301
- object: 'eval',
1302
- name: typeof params.name === 'string' ? params.name : `eval-${id}`,
1303
- created_at: nowEpoch(req.occurredAt),
1304
- data_source_config: params.data_source_config,
1305
- testing_criteria: params.testing_criteria,
1306
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1307
- };
1308
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'eval.create', subjectType: 'eval', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1309
- return { status: 200, body: idView(resource) };
1310
- }
1311
- function evalView(r: Record<string, unknown>): Record<string, unknown> {
1312
- return { id: r.id, ...strip(r) };
1313
- }
1314
- // A run records a synthetic per-model usage + a result_counts of passed items. The twin grades a
1315
- // fixed single datasource item (the twin cannot fetch a real dataset) → 1 passed item.
1316
- async function createEvalRun(evalId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1317
- if (params.data_source === undefined) return invalidRequest("you must provide a data_source parameter", 'data_source');
1318
- const id = nextId('evalrun', 'evalrun', req.root);
1319
- const created = nowEpoch(req.occurredAt);
1320
- const model = typeof (params.data_source as { model?: unknown })?.model === 'string' ? String((params.data_source as { model?: unknown }).model) : 'gpt-4o';
1321
- const fields = {
1322
- object: 'eval.run',
1323
- eval_id: evalId,
1324
- name: typeof params.name === 'string' ? params.name : `run-${id}`,
1325
- created_at: created,
1326
- status: 'completed',
1327
- model,
1328
- data_source: params.data_source,
1329
- result_counts: { total: 1, errored: 0, failed: 0, passed: 1 },
1330
- per_model_usage: [{ model_name: model, invocation_count: 1, prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cached_tokens: 0 }],
1331
- per_testing_criteria_results: [{ testing_criteria: 'twin-stub-grader', passed: 1, failed: 0 }],
1332
- report_url: `https://twin.invalid/evals/${evalId}/runs/${id}`,
1333
- metadata: (params.metadata && typeof params.metadata === 'object') ? params.metadata : {},
1334
- error: null,
1335
- };
1336
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'eval_run.create', subjectType: 'eval_run', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1337
- return { status: 200, body: evalRunView(resource) };
1338
- }
1339
- function evalRunView(r: Record<string, unknown>): Record<string, unknown> {
1340
- return { id: r.id, ...strip(r) };
1341
- }
1342
- // The output items for a completed run: a single deterministic passed item (the twin's stub
1343
- // output graded by a deterministic stub grader). Shape: object:'eval.run.output_item'.
1344
- function evalRunOutputItems(run: Record<string, unknown>): Array<Record<string, unknown>> {
1345
- const created = Number(run.created_at ?? 0);
1346
- return [{
1347
- id: `evalitem-${run.id}-1`,
1348
- object: 'eval.run.output_item',
1349
- created_at: created,
1350
- run_id: String(run.id),
1351
- eval_id: String(run.eval_id),
1352
- status: 'pass',
1353
- datasource_item_id: 0,
1354
- datasource_item: { item: { input: '[twin-stub] datasource item' } },
1355
- results: [{ name: 'twin-stub-grader', passed: true, score: 1.0 }],
1356
- sample: { input: [], output: [{ role: 'assistant', content: '[twin-stub] eval sample output (no real model run)' }], finish_reason: 'stop', model: String(run.model ?? 'gpt-4o'), usage: { total_tokens: 0, completion_tokens: 0, prompt_tokens: 0, cached_tokens: 0 }, error: null },
1357
- }];
1358
- }
1359
-
1360
- // ── Containers API (code-interpreter sandboxes: container → container files) ──────────────
1361
- // A container is an isolated sandbox for the code_interpreter tool. The twin cannot run a real
1362
- // sandbox, so a container is created `running` and its files are stored faithfully (metadata +
1363
- // supplied text content, like the Files API). The SHAPES are vendor-faithful.
1364
- async function createContainer(params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1365
- if (params.name === undefined || params.name === '') return invalidRequest("you must provide a name parameter", 'name');
1366
- const id = nextId('container', 'cntr', req.root);
1367
- const created = nowEpoch(req.occurredAt);
1368
- const fields = {
1369
- object: 'container',
1370
- name: String(params.name),
1371
- created_at: created,
1372
- status: 'running',
1373
- last_active_at: created,
1374
- expires_after: (params.expires_after && typeof params.expires_after === 'object') ? params.expires_after : { anchor: 'last_active_at', minutes: 20 },
1375
- };
1376
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'container.create', subjectType: 'container', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1377
- return { status: 200, body: containerView(resource) };
1378
- }
1379
- function containerView(r: Record<string, unknown>): Record<string, unknown> {
1380
- return { id: r.id, ...strip(r) };
1381
- }
1382
- async function createContainerFile(containerId: string, params: Record<string, unknown>, req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1383
- // The server adapts a multipart upload (or a JSON { file_id } reference) into JSON; the twin
1384
- // accepts either an inline `content`+`path` (text) or a `file_id` referencing a stored File.
1385
- const seq = rows('container_file', req.root).filter((r) => r.container_id === containerId).length + 1;
1386
- const id = `cfile-twin-${containerId}-${seq}`;
1387
- const created = nowEpoch(req.occurredAt);
1388
- let content = '';
1389
- let source = 'user';
1390
- if (typeof params.file_id === 'string' && params.file_id) {
1391
- const f = getRow('file', params.file_id, req.root);
1392
- if (!f || f._deleted) return notFound(`No such File object: ${String(params.file_id)}`);
1393
- content = String(f._content ?? '');
1394
- source = 'file_id';
1395
- } else if (typeof params.content === 'string') {
1396
- content = params.content;
1397
- }
1398
- const path = typeof params.path === 'string' && params.path ? params.path : `/mnt/data/${id}`;
1399
- const fields = {
1400
- object: 'container.file',
1401
- container_id: containerId,
1402
- created_at: created,
1403
- bytes: content.length,
1404
- path,
1405
- source,
1406
- _content: content,
1407
- };
1408
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'container_file.create', subjectType: 'container_file', subjectId: id, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1409
- return { status: 200, body: containerFileView(resource) };
1410
- }
1411
- function containerFileView(r: Record<string, unknown>): Record<string, unknown> {
1412
- return { id: r.id, ...strip(r) };
1413
- }
1414
-
1415
- // ── public entry: cross-cutting protocol (auth / rate-limit / idempotency) then route ─────
1416
- // The HTTP server passes request headers, so on the live wire every call is auth/rate-limit/
1417
- // idempotency-checked; in-process trusted calls (capability verify, connector) omit `headers`/
1418
- // `apiKey` and are NOT gated. Idempotency-Key dedup wraps the WHOLE request so any successful
1419
- // mutation is replayed verbatim on a re-issue with the same key.
1153
+ const format = SPEECH[String(params.response_format ?? 'mp3')];
1154
+ if (!format) return invalidRequest("'response_format' must be one of 'mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'", 'response_format');
1155
+ return { status: 200, audio: format.file(), type: format.type };
1156
+ }
1157
+
1158
+ // ── public entry: cross-cutting protocol (auth / rate-limit) then route ─────────────────────
1159
+ // The HTTP server passes request headers, so on the live wire every call is auth/rate-limit-
1160
+ // checked; in-process trusted calls (capability verify, connector) omit `headers`/`apiKey` and are
1161
+ // NOT gated.
1162
+ /** OpenAI's API as one call: the request goes through the pack's own dispatch (the derived dispatch over its
1163
+ * semantics, its derived core and the hand-written routes below), so a caller that holds no HTTP
1164
+ * server reaches exactly what an SDK does. A caller that presents no credential is a trusted
1165
+ * in-process caller and is not held to the auth rule; `sseSink` receives a stream's events. */
1420
1166
  export async function handleOpenAITwinRequest(req: OpenAIRequest): Promise<OpenAIResponseEnvelope> {
1167
+ const twin = createOpenAITwinFetch({
1168
+ ...(req.root !== undefined ? { root: req.root } : {}),
1169
+ readOnly: req.readOnly ?? false,
1170
+ ...(req.scenarioEngine ? { scenarioEngine: req.scenarioEngine } : {}),
1171
+ ...(req.occurredAt ? { clock: () => req.occurredAt! } : {}),
1172
+ });
1173
+ const headers: Record<string, string> = { ...(req.headers ?? {}) };
1174
+ if (req.apiKey !== undefined) headers.authorization = `Bearer ${req.apiKey}`;
1175
+ else if (req.headers === undefined) headers.authorization = 'Bearer sk-twin-in-process';
1421
1176
  const method = req.method.toUpperCase();
1422
-
1423
- // Modeled authentication (401). Gated only when the request carries an auth surface.
1424
- if (req.headers !== undefined || req.apiKey !== undefined) {
1425
- const authErr = checkAuth(req);
1426
- if (authErr) return authErr;
1427
- }
1428
- // Modeled rate limiting (429) — deterministic opt-in trigger header.
1429
- if (rateLimitTriggered(req)) return rateLimitError();
1430
-
1431
- // Idempotency-Key: a re-issue with the same key replays the prior response byte-for-byte. Only
1432
- // mutations (non-GET) are keyed (matching the vendor), and only 2xx results are cached.
1433
- const idemKey = method !== 'GET' ? req.headers?.['idempotency-key'] : undefined;
1434
- if (idemKey) {
1435
- const cached = getIdempotentResult(idemKey, req.root);
1436
- if (cached) return cached;
1437
- const result = await routeOpenAI(req, method);
1438
- if (result.status >= 200 && result.status < 300) await storeIdempotentResult(idemKey, result, req);
1439
- return result;
1440
- }
1441
- return routeOpenAI(req, method);
1442
- }
1443
-
1444
- // ── router ──────────────────────────────────────────────────────────────────────────────
1445
- async function routeOpenAI(req: OpenAIRequest, method: string): Promise<OpenAIResponseEnvelope> {
1446
- const path = (req.path.split('?')[0] ?? '/').replace(/\/+$/, '') || '/';
1447
- const seg = path.replace(/^\/+/, '').split('/'); // ["v1","chat","completions",...]
1448
- const params = parseJson(req.body);
1449
- const dec = (s: string) => decodeURIComponent(s);
1450
-
1451
- // D3: a read-only twin rejects any mutation with a vendor-shaped error.
1452
- if (req.readOnly && method !== 'GET') {
1453
- return { status: 405, body: errBody('invalid_request_error', 'twin is read-only; omit readOnly to accept writes', 'method_not_allowed') };
1454
- }
1455
-
1456
- // ---- models (static catalog + fine-tuned models minted by fine-tuning jobs) ----
1457
- if (path === '/v1/models' && method === 'GET') {
1458
- // List the static catalog plus every (non-deleted) fine-tuned model produced by a job.
1459
- return { status: 200, body: { object: 'list', data: [...OPENAI_MODELS, ...fineTunedModels(req.root)] } };
1460
- }
1461
- if (seg[1] === 'models' && seg.length === 3 && method === 'GET') {
1462
- const mid = dec(seg[2]!);
1463
- const m = findModel(mid) ?? fineTunedModels(req.root).find((fm) => fm.id === mid);
1464
- return m ? { status: 200, body: m } : notFound(`The model '${mid}' does not exist`, 'model_not_found');
1465
- }
1466
- // Delete a fine-tuned model (only models you own — base catalog models can't be deleted).
1467
- if (seg[1] === 'models' && seg.length === 3 && method === 'DELETE') {
1468
- const mid = dec(seg[2]!);
1469
- if (findModel(mid)) return invalidRequest(`The model '${mid}' cannot be deleted`, 'model', 'model_not_deletable');
1470
- const ft = fineTunedModels(req.root).find((fm) => fm.id === mid);
1471
- if (!ft) return notFound(`The model '${mid}' does not exist`, 'model_not_found');
1472
- await applyTwinWrite(SERVICE, { operation: 'model.delete', subjectType: 'model', subjectId: mid, fields: { _deleted: true, object: 'model' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1473
- return { status: 200, body: { id: mid, object: 'model', deleted: true } };
1474
- }
1475
-
1476
- // ---- stored chat completions (store:true) — retrieve / list / messages / delete ----
1477
- // (must precede the generic create route below; these are GET/DELETE on the same prefix.)
1478
- if (path === '/v1/chat/completions' && method === 'GET') {
1479
- const items = rows('chat_completion', req.root).filter((r) => !r._deleted).map(storedChatView).sort((a, b) => Number(b.created) - Number(a.created));
1480
- return { status: 200, body: paginate(items, req.path) };
1481
- }
1482
- if (seg[1] === 'chat' && seg[2] === 'completions' && seg.length === 4 && method === 'GET') {
1483
- const c = getRow('chat_completion', dec(seg[3]!), req.root);
1484
- return c && !c._deleted ? { status: 200, body: storedChatView(c) } : notFound(`No chat completion found with id '${dec(seg[3]!)}'.`);
1485
- }
1486
- if (seg[1] === 'chat' && seg[2] === 'completions' && seg.length === 5 && seg[4] === 'messages' && method === 'GET') {
1487
- const c = getRow('chat_completion', dec(seg[3]!), req.root);
1488
- if (!c || c._deleted) return notFound(`No chat completion found with id '${dec(seg[3]!)}'.`);
1489
- const msgs = ((c as { _input_messages?: unknown })._input_messages as unknown[]) ?? [];
1490
- return { status: 200, body: paginate(msgs as Array<Record<string, unknown>>, req.path) };
1491
- }
1492
- if (seg[1] === 'chat' && seg[2] === 'completions' && seg.length === 4 && method === 'DELETE') {
1493
- const cid = dec(seg[3]!);
1494
- const c = getRow('chat_completion', cid, req.root);
1495
- if (!c || c._deleted) return notFound(`No chat completion found with id '${cid}'.`);
1496
- await applyTwinWrite(SERVICE, { operation: 'chat_completion.delete', subjectType: 'chat_completion', subjectId: cid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1497
- return { status: 200, body: { id: cid, object: 'chat.completion.deleted', deleted: true } };
1498
- }
1499
-
1500
- // ---- chat completions (the generative stub; envelope is faithful) ----
1501
- if (path === '/v1/chat/completions' && method === 'POST') {
1502
- const validated = validateChat(params);
1503
- if ('error' in validated) return validated.error;
1504
- const args = validated.args;
1505
- let result: ChatCompletion;
1506
- if (args.stream && req.sseSink) result = streamChat(args, req.sseSink, req.occurredAt, req.scenarioEngine);
1507
- else result = buildChatCompletion(args, req.occurredAt, req.scenarioEngine);
1508
- // store:true → persist the completion so it can be retrieved/listed later (stored completions).
1509
- if (args.store) await storeChatCompletion(result, args, req);
1510
- await recordUsage(req, 'completions', result.model, result.usage.prompt_tokens, result.usage.completion_tokens);
1511
- return { status: 200, body: result };
1512
- }
1513
-
1514
- // ---- responses API ----
1515
- if (path === '/v1/responses' && method === 'POST') {
1516
- const validated = validateResponses(params);
1517
- if ('error' in validated) return validated.error;
1518
- const args = validated.args;
1519
- // previous_response_id chaining: the referenced response must exist; its assistant output is
1520
- // folded into the prompt context (so usage reflects the chained history — server-side state).
1521
- if (args.previousResponseId) {
1522
- const prior = getRow('response', args.previousResponseId, req.root);
1523
- if (!prior || prior._deleted) return notFound(`Response with id '${args.previousResponseId}' not found.`);
1524
- const priorText = String((prior as { output_text?: unknown }).output_text ?? '');
1525
- args.messages = [{ role: 'assistant', content: priorText }, ...args.messages];
1526
- }
1527
- // Deterministic stored-response id (hash of model+input+chain) so retrieval is assertable.
1528
- const suffix = responseSuffix(args);
1529
- // background:true → the response is created `queued` and processed asynchronously. The twin has
1530
- // nothing to run in the background, so it persists a queued response that the FIRST poll
1531
- // (GET /v1/responses/:id) transitions to `completed` (computing the real stub output then). The
1532
- // queued envelope carries no output yet; background implies store (so it can be polled/cancelled).
1533
- if (args.background) {
1534
- const queued = await storeQueuedResponse(args, req, suffix);
1535
- return { status: 200, body: responseView(queued) };
1536
- }
1537
- if (args.stream) {
1538
- const resp = req.sseSink ? streamResponse(args, req.sseSink, req.occurredAt, suffix) : buildResponse(args, req.occurredAt, suffix);
1539
- if (args.store) await storeResponse(resp, args, req);
1540
- await recordUsage(req, 'responses', resp.model, resp.usage.input_tokens, resp.usage.output_tokens);
1541
- return { status: 200, body: resp };
1542
- }
1543
- const resp = buildResponse(args, req.occurredAt, suffix);
1544
- if (args.store) await storeResponse(resp, args, req);
1545
- await recordUsage(req, 'responses', resp.model, resp.usage.input_tokens, resp.usage.output_tokens);
1546
- return { status: 200, body: resp };
1547
- }
1548
- // Responses: retrieve / delete a stored response by id.
1549
- if (seg[1] === 'responses' && seg.length === 3 && method === 'GET') {
1550
- const r = getRow('response', dec(seg[2]!), req.root);
1551
- if (!r || r._deleted) return notFound(`Response with id '${dec(seg[2]!)}' not found.`);
1552
- // A queued background response is processed on first poll: transition queued → completed,
1553
- // computing the real stub output + usage at that point (then record billable usage once).
1554
- if (r.status === 'queued') {
1555
- const completed = await completeQueuedResponse(r, req);
1556
- return { status: 200, body: responseView(completed) };
1557
- }
1558
- return { status: 200, body: responseView(r) };
1559
- }
1560
- // Responses: cancel a background response (only a queued/in_progress one can be cancelled).
1561
- if (seg[1] === 'responses' && seg.length === 4 && seg[3] === 'cancel' && method === 'POST') {
1562
- const rid = dec(seg[2]!);
1563
- const r = getRow('response', rid, req.root);
1564
- if (!r || r._deleted) return notFound(`Response with id '${rid}' not found.`);
1565
- if (r.status !== 'queued' && r.status !== 'in_progress') {
1566
- return invalidRequest(`Cannot cancel a response with status '${String(r.status)}'.`, null, 'invalid_status');
1567
- }
1568
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'response.cancel', subjectType: 'response', subjectId: rid, fields: { status: 'cancelled' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1569
- return { status: 200, body: responseView(resource) };
1570
- }
1571
- if (seg[1] === 'responses' && seg.length === 3 && method === 'DELETE') {
1572
- const rid = dec(seg[2]!);
1573
- const r = getRow('response', rid, req.root);
1574
- if (!r || r._deleted) return notFound(`Response with id '${rid}' not found.`);
1575
- await applyTwinWrite(SERVICE, { operation: 'response.delete', subjectType: 'response', subjectId: rid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1576
- return { status: 200, body: { id: rid, object: 'response.deleted', deleted: true } };
1577
- }
1578
- // Responses: list the input items of a stored response (the user turns folded into it).
1579
- if (seg[1] === 'responses' && seg.length === 4 && seg[3] === 'input_items' && method === 'GET') {
1580
- const r = getRow('response', dec(seg[2]!), req.root);
1581
- if (!r || r._deleted) return notFound(`Response with id '${dec(seg[2]!)}' not found.`);
1582
- const items = ((r as { _input_items?: unknown })._input_items as unknown[]) ?? [];
1583
- return { status: 200, body: { object: 'list', data: items, has_more: false } };
1584
- }
1585
-
1586
- // ---- embeddings ----
1587
- if (path === '/v1/embeddings' && method === 'POST') {
1588
- const res = handleEmbeddings(params);
1589
- if (res.status === 200) {
1590
- const b = res.body as EmbeddingResponse;
1591
- await recordUsage(req, 'embeddings', b.model, b.usage.prompt_tokens, 0);
1592
- }
1593
- return res;
1594
- }
1595
-
1596
- // ---- moderations ----
1597
- if (path === '/v1/moderations' && method === 'POST') {
1598
- return handleModerations(params, req.occurredAt);
1599
- }
1600
-
1601
- // ---- images ----
1602
- if (path === '/v1/images/generations' && method === 'POST') {
1603
- return handleImages(params, req.occurredAt);
1604
- }
1605
- if (path === '/v1/images/edits' && method === 'POST') return handleImageEdit(params, req.occurredAt);
1606
- if (path === '/v1/images/variations' && method === 'POST') return handleImageVariation(params, req.occurredAt);
1607
-
1608
- // ---- audio ----
1609
- if (path === '/v1/audio/transcriptions' && method === 'POST') return handleTranscription(params, false);
1610
- if (path === '/v1/audio/translations' && method === 'POST') return handleTranscription(params, true);
1611
- if (path === '/v1/audio/speech' && method === 'POST') return handleSpeech(params);
1612
-
1613
- // ---- files (stateful) ----
1614
- if (path === '/v1/files' && method === 'POST') return createFile(params, req);
1615
- if (path === '/v1/files' && method === 'GET') {
1616
- const items = rows('file', req.root).filter((r) => !r._deleted).map(fileView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1617
- return { status: 200, body: paginate(items, req.path) };
1618
- }
1619
- if (seg[1] === 'files' && seg.length === 3 && method === 'GET') {
1620
- const f = getRow('file', dec(seg[2]!), req.root);
1621
- return f && !f._deleted ? { status: 200, body: fileView(f) } : notFound(`No such File object: ${dec(seg[2]!)}`);
1622
- }
1623
- if (seg[1] === 'files' && seg.length === 4 && seg[3] === 'content' && method === 'GET') {
1624
- const f = getRow('file', dec(seg[2]!), req.root);
1625
- if (!f || f._deleted) return notFound(`No such File object: ${dec(seg[2]!)}`);
1626
- return { status: 200, body: (f._content as string) ?? '' };
1627
- }
1628
- if (seg[1] === 'files' && seg.length === 3 && method === 'DELETE') {
1629
- const id = dec(seg[2]!);
1630
- const f = getRow('file', id, req.root);
1631
- if (!f) return notFound(`No such File object: ${id}`);
1632
- await applyTwinWrite(SERVICE, { operation: 'file.delete', subjectType: 'file', subjectId: id, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1633
- return { status: 200, body: { id, object: 'file', deleted: true } };
1634
- }
1635
-
1636
- // ---- uploads (multipart large-file parts, stateful) ----
1637
- if (path === '/v1/uploads' && method === 'POST') return createUpload(params, req);
1638
- if (seg[1] === 'uploads' && seg.length === 4 && seg[3] === 'parts' && method === 'POST') return addUploadPart(dec(seg[2]!), params, req);
1639
- if (seg[1] === 'uploads' && seg.length === 4 && seg[3] === 'complete' && method === 'POST') return completeUpload(dec(seg[2]!), params, req);
1640
- if (seg[1] === 'uploads' && seg.length === 4 && seg[3] === 'cancel' && method === 'POST') {
1641
- const uid = dec(seg[2]!);
1642
- const up = getRow('upload', uid, req.root);
1643
- if (!up || up.status !== 'pending') return notFound(`No such Upload object: ${uid}`);
1644
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'upload.cancel', subjectType: 'upload', subjectId: uid, fields: { status: 'cancelled' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1645
- return { status: 200, body: uploadView(resource) };
1646
- }
1647
-
1648
- // ---- batches (stateful) ----
1649
- if (path === '/v1/batches' && method === 'POST') return createBatch(params, req);
1650
- if (path === '/v1/batches' && method === 'GET') {
1651
- const items = rows('batch', req.root).map(batchView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1652
- return { status: 200, body: paginate(items, req.path) };
1653
- }
1654
- if (seg[1] === 'batches' && seg.length === 3 && method === 'GET') {
1655
- const b = getRow('batch', dec(seg[2]!), req.root);
1656
- return b ? { status: 200, body: batchView(b) } : notFound(`No such Batch object: ${dec(seg[2]!)}`);
1657
- }
1658
- if (seg[1] === 'batches' && seg.length === 4 && seg[3] === 'cancel' && method === 'POST') {
1659
- const id = dec(seg[2]!);
1660
- const b = getRow('batch', id, req.root);
1661
- if (!b) return notFound(`No such Batch object: ${id}`);
1662
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'batch.cancel', subjectType: 'batch', subjectId: id, fields: { status: 'cancelled', cancelled_at: nowEpoch(req.occurredAt) }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1663
- return { status: 200, body: batchView(resource) };
1664
- }
1665
-
1666
- // ---- moderations handled above ----
1667
-
1668
- // ---- fine-tuning jobs (stateful) ----
1669
- if (path === '/v1/fine_tuning/jobs' && method === 'POST') return createFineTune(params, req);
1670
- if (path === '/v1/fine_tuning/jobs' && method === 'GET') {
1671
- const items = rows('fine_tuning_job', req.root).map(ftView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1672
- return { status: 200, body: paginate(items, req.path) };
1673
- }
1674
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 4 && method === 'GET') {
1675
- const j = getRow('fine_tuning_job', dec(seg[3]!), req.root);
1676
- return j ? { status: 200, body: ftView(j) } : notFound(`No such fine-tuning job: ${dec(seg[3]!)}`);
1677
- }
1678
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 5 && seg[4] === 'events' && method === 'GET') {
1679
- const j = getRow('fine_tuning_job', dec(seg[3]!), req.root);
1680
- if (!j) return notFound(`No such fine-tuning job: ${dec(seg[3]!)}`);
1681
- // Deterministic synthetic event stream for a completed twin job.
1682
- const created = Number(j.created_at ?? 0);
1683
- const events = [
1684
- { object: 'fine_tuning.job.event', id: `ftevent-${j.id}-1`, created_at: created, level: 'info', message: 'Created fine-tuning job', type: 'message' },
1685
- { object: 'fine_tuning.job.event', id: `ftevent-${j.id}-2`, created_at: created, level: 'info', message: 'Fine-tuning job successfully completed (twin stub)', type: 'message' },
1686
- ];
1687
- return { status: 200, body: { object: 'list', data: events, has_more: false } };
1688
- }
1689
- // Fine-tuning checkpoints: a completed job exposes its training checkpoints (one per epoch). The
1690
- // twin synthesizes a deterministic checkpoint per succeeded job (shape faithful; metrics are stubs).
1691
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 5 && seg[4] === 'checkpoints' && method === 'GET') {
1692
- const j = getRow('fine_tuning_job', dec(seg[3]!), req.root);
1693
- if (!j) return notFound(`No such fine-tuning job: ${dec(seg[3]!)}`);
1694
- const created = Number(j.created_at ?? 0);
1695
- // succeeded jobs have a final checkpoint pointing at the fine-tuned model; cancelled jobs have none.
1696
- const checkpoints = j.status === 'succeeded'
1697
- ? [{
1698
- object: 'fine_tuning.job.checkpoint',
1699
- id: `ftckpt-${j.id}-1`,
1700
- created_at: created,
1701
- fine_tuned_model_checkpoint: String(j.fine_tuned_model ?? `ft:twin::${j.id}:ckpt-step-1`),
1702
- fine_tuning_job_id: String(j.id),
1703
- metrics: { step: 1, train_loss: 0, train_mean_token_accuracy: 1, full_valid_loss: 0, full_valid_mean_token_accuracy: 1 },
1704
- step_number: 1,
1705
- }]
1706
- : [];
1707
- return { status: 200, body: { object: 'list', data: checkpoints, has_more: false, first_id: checkpoints[0]?.id ?? null, last_id: checkpoints[0]?.id ?? null } };
1708
- }
1709
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 5 && seg[4] === 'cancel' && method === 'POST') {
1710
- const id = dec(seg[3]!);
1711
- const j = getRow('fine_tuning_job', id, req.root);
1712
- if (!j) return notFound(`No such fine-tuning job: ${id}`);
1713
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'fine_tuning_job.cancel', subjectType: 'fine_tuning_job', subjectId: id, fields: { status: 'cancelled' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1714
- return { status: 200, body: ftView(resource) };
1715
- }
1716
- // Pause a fine-tuning job → status 'paused' (a deployable checkpoint is preserved). Only a
1717
- // running/queued/succeeded job can be paused; a cancelled job can't (vendor 400).
1718
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 5 && seg[4] === 'pause' && method === 'POST') {
1719
- const id = dec(seg[3]!);
1720
- const j = getRow('fine_tuning_job', id, req.root);
1721
- if (!j) return notFound(`No such fine-tuning job: ${id}`);
1722
- if (j.status === 'cancelled' || j.status === 'failed') return invalidRequest(`Cannot pause a job with status '${String(j.status)}'.`, null, 'invalid_status');
1723
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'fine_tuning_job.pause', subjectType: 'fine_tuning_job', subjectId: id, fields: { status: 'paused' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1724
- return { status: 200, body: ftView(resource) };
1725
- }
1726
- // Resume a paused fine-tuning job → status back to 'succeeded' (the twin's terminal state, since
1727
- // it cannot actually train). Only a paused job can be resumed (vendor 400 otherwise).
1728
- if (seg[1] === 'fine_tuning' && seg[2] === 'jobs' && seg.length === 5 && seg[4] === 'resume' && method === 'POST') {
1729
- const id = dec(seg[3]!);
1730
- const j = getRow('fine_tuning_job', id, req.root);
1731
- if (!j) return notFound(`No such fine-tuning job: ${id}`);
1732
- if (j.status !== 'paused') return invalidRequest(`Cannot resume a job with status '${String(j.status)}'.`, null, 'invalid_status');
1733
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'fine_tuning_job.resume', subjectType: 'fine_tuning_job', subjectId: id, fields: { status: 'succeeded' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1734
- return { status: 200, body: ftView(resource) };
1735
- }
1736
-
1737
- // ---- vector stores (stateful) ----
1738
- if (path === '/v1/vector_stores' && method === 'POST') return createVectorStore(params, req);
1739
- if (path === '/v1/vector_stores' && method === 'GET') {
1740
- const items = rows('vector_store', req.root).filter((r) => !r._deleted).map(vsView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1741
- return { status: 200, body: paginate(items, req.path) };
1742
- }
1743
- if (seg[1] === 'vector_stores' && seg.length === 3 && method === 'GET') {
1744
- const v = getRow('vector_store', dec(seg[2]!), req.root);
1745
- return v && !v._deleted ? { status: 200, body: vsView(v) } : notFound(`No such vector store: ${dec(seg[2]!)}`);
1746
- }
1747
- if (seg[1] === 'vector_stores' && seg.length === 3 && method === 'DELETE') {
1748
- const id = dec(seg[2]!);
1749
- const v = getRow('vector_store', id, req.root);
1750
- if (!v) return notFound(`No such vector store: ${id}`);
1751
- await applyTwinWrite(SERVICE, { operation: 'vector_store.delete', subjectType: 'vector_store', subjectId: id, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1752
- return { status: 200, body: { id, object: 'vector_store.deleted', deleted: true } };
1753
- }
1754
- if (seg[1] === 'vector_stores' && seg.length === 4 && seg[3] === 'files' && method === 'POST') {
1755
- const storeId = dec(seg[2]!);
1756
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1757
- if (params.file_id === undefined || params.file_id === '') return invalidRequest("you must provide a file_id parameter", 'file_id');
1758
- const view = await addVectorStoreFile(storeId, String(params.file_id), req);
1759
- return { status: 200, body: view };
1760
- }
1761
- if (seg[1] === 'vector_stores' && seg.length === 4 && seg[3] === 'files' && method === 'GET') {
1762
- const storeId = dec(seg[2]!);
1763
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1764
- const all = rows('vector_store_file', req.root).filter((r) => r.vector_store_id === storeId && !r._deleted).map(vsFileView);
1765
- return { status: 200, body: paginate(all, req.path) };
1766
- }
1767
- // Vector store FILE BATCHES: bulk-attach a list of file_ids to a store in one call. The twin
1768
- // attaches each file as a vector_store.file child, then returns a batch object (status completed,
1769
- // since the twin has nothing to process async). Retrieve + list-files re-read the batch.
1770
- if (seg[1] === 'vector_stores' && seg.length === 4 && seg[3] === 'file_batches' && method === 'POST') {
1771
- const storeId = dec(seg[2]!);
1772
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1773
- if (!Array.isArray(params.file_ids) || params.file_ids.length === 0) return invalidRequest("you must provide a non-empty file_ids array", 'file_ids');
1774
- const fileIds = (params.file_ids as unknown[]).map(String);
1775
- const batchId = nextId('vector_store_file_batch', 'vsfb', req.root);
1776
- for (const fid of fileIds) await addVectorStoreFile(storeId, fid, req, batchId);
1777
- const created = nowEpoch(req.occurredAt);
1778
- const fields = {
1779
- object: 'vector_store.file_batch',
1780
- vector_store_id: storeId,
1781
- created_at: created,
1782
- status: 'completed',
1783
- file_counts: { in_progress: 0, completed: fileIds.length, failed: 0, cancelled: 0, total: fileIds.length },
1784
- _file_ids: fileIds,
1785
- };
1786
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'vector_store_file_batch.create', subjectType: 'vector_store_file_batch', subjectId: batchId, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1787
- return { status: 200, body: vsBatchView(resource) };
1788
- }
1789
- if (seg[1] === 'vector_stores' && seg.length === 5 && seg[3] === 'file_batches' && method === 'GET') {
1790
- const storeId = dec(seg[2]!);
1791
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1792
- const b = getRow('vector_store_file_batch', dec(seg[4]!), req.root);
1793
- return b && b.vector_store_id === storeId ? { status: 200, body: vsBatchView(b) } : notFound(`No such file batch: ${dec(seg[4]!)}`);
1794
- }
1795
- if (seg[1] === 'vector_stores' && seg.length === 6 && seg[3] === 'file_batches' && seg[5] === 'files' && method === 'GET') {
1796
- const storeId = dec(seg[2]!);
1797
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1798
- const batchId = dec(seg[4]!);
1799
- if (!getRow('vector_store_file_batch', batchId, req.root)) return notFound(`No such file batch: ${batchId}`);
1800
- const all = rows('vector_store_file', req.root).filter((r) => r.vector_store_id === storeId && r._batch_id === batchId && !r._deleted).map(vsFileView);
1801
- return { status: 200, body: paginate(all, req.path) };
1802
- }
1803
-
1804
- // Vector store search: deterministic ranked results over the store's attached files.
1805
- if (seg[1] === 'vector_stores' && seg.length === 4 && seg[3] === 'search' && method === 'POST') {
1806
- const storeId = dec(seg[2]!);
1807
- if (!getRow('vector_store', storeId, req.root)) return notFound(`No such vector store: ${storeId}`);
1808
- if (params.query === undefined || params.query === '') return invalidRequest("you must provide a query parameter", 'query');
1809
- const query = typeof params.query === 'string' ? params.query : JSON.stringify(params.query);
1810
- const files = rows('vector_store_file', req.root).filter((r) => r.vector_store_id === storeId && !r._deleted);
1811
- const maxResults = Number.isInteger(Number(params.max_num_results)) ? Number(params.max_num_results) : 10;
1812
- const ranked = files
1813
- .map((f) => ({ file_id: String((f as { file_id?: unknown }).file_id ?? ''), score: searchScore(query, String((f as { file_id?: unknown }).file_id ?? '')) }))
1814
- .sort((a, b) => b.score - a.score)
1815
- .slice(0, Math.max(1, maxResults))
1816
- .map((r) => ({ file_id: r.file_id, filename: r.file_id, score: r.score, attributes: {}, content: [{ type: 'text', text: `[twin-stub] deterministic pseudo-match for "${query}" in ${r.file_id} (no real semantic search)` }] }));
1817
- return { status: 200, body: { object: 'vector_store.search_results.page', search_query: query, data: ranked, has_more: false, next_page: null } };
1818
- }
1819
-
1820
- // ---- assistants (beta, stateful) ----
1821
- if (path === '/v1/assistants' && method === 'POST') return createAssistant(params, req);
1822
- if (path === '/v1/assistants' && method === 'GET') {
1823
- const items = rows('assistant', req.root).filter((r) => !r._deleted).map(idView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1824
- return { status: 200, body: paginate(items, req.path) };
1825
- }
1826
- if (seg[1] === 'assistants' && seg.length === 3 && method === 'GET') {
1827
- const a = getRow('assistant', dec(seg[2]!), req.root);
1828
- return a && !a._deleted ? { status: 200, body: idView(a) } : notFound(`No assistant found with id '${dec(seg[2]!)}'.`);
1829
- }
1830
- if (seg[1] === 'assistants' && seg.length === 3 && method === 'POST') {
1831
- const aid = dec(seg[2]!);
1832
- const a = getRow('assistant', aid, req.root);
1833
- if (!a || a._deleted) return notFound(`No assistant found with id '${aid}'.`);
1834
- const fields = pickUpdate(params, ['name', 'description', 'model', 'instructions', 'tools', 'metadata', 'temperature', 'top_p', 'response_format']);
1835
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'assistant.update', subjectType: 'assistant', subjectId: aid, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1836
- return { status: 200, body: idView(resource) };
1837
- }
1838
- if (seg[1] === 'assistants' && seg.length === 3 && method === 'DELETE') {
1839
- const aid = dec(seg[2]!);
1840
- const a = getRow('assistant', aid, req.root);
1841
- if (!a || a._deleted) return notFound(`No assistant found with id '${aid}'.`);
1842
- await applyTwinWrite(SERVICE, { operation: 'assistant.delete', subjectType: 'assistant', subjectId: aid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1843
- return { status: 200, body: { id: aid, object: 'assistant.deleted', deleted: true } };
1844
- }
1845
-
1846
- // ---- threads (+ messages) (beta, stateful) ----
1847
- if (path === '/v1/threads' && method === 'POST') return createThread(params, req);
1848
- if (seg[1] === 'threads' && seg.length === 3 && method === 'GET') {
1849
- const t = getRow('thread', dec(seg[2]!), req.root);
1850
- return t && !t._deleted ? { status: 200, body: idView(t) } : notFound(`No thread found with id '${dec(seg[2]!)}'.`);
1851
- }
1852
- if (seg[1] === 'threads' && seg.length === 3 && method === 'POST') {
1853
- const tid = dec(seg[2]!);
1854
- const t = getRow('thread', tid, req.root);
1855
- if (!t || t._deleted) return notFound(`No thread found with id '${tid}'.`);
1856
- const fields = pickUpdate(params, ['metadata', 'tool_resources']);
1857
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'thread.update', subjectType: 'thread', subjectId: tid, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1858
- return { status: 200, body: idView(resource) };
1859
- }
1860
- if (seg[1] === 'threads' && seg.length === 3 && method === 'DELETE') {
1861
- const tid = dec(seg[2]!);
1862
- const t = getRow('thread', tid, req.root);
1863
- if (!t || t._deleted) return notFound(`No thread found with id '${tid}'.`);
1864
- await applyTwinWrite(SERVICE, { operation: 'thread.delete', subjectType: 'thread', subjectId: tid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1865
- return { status: 200, body: { id: tid, object: 'thread.deleted', deleted: true } };
1866
- }
1867
- if (seg[1] === 'threads' && seg.length === 4 && seg[3] === 'messages' && method === 'POST') {
1868
- const tid = dec(seg[2]!);
1869
- if (!getRow('thread', tid, req.root)) return notFound(`No thread found with id '${tid}'.`);
1870
- if (params.content === undefined) return invalidRequest("you must provide a content parameter", 'content');
1871
- const view = await addThreadMessage(tid, params, req);
1872
- return { status: 200, body: view };
1873
- }
1874
- if (seg[1] === 'threads' && seg.length === 4 && seg[3] === 'messages' && method === 'GET') {
1875
- const tid = dec(seg[2]!);
1876
- if (!getRow('thread', tid, req.root)) return notFound(`No thread found with id '${tid}'.`);
1877
- const all = rows('message', req.root).filter((r) => r.thread_id === tid && !r._deleted).map(msgView).sort((a, b) => Number(b.created_at) - Number(a.created_at) || String(b.id).localeCompare(String(a.id)));
1878
- return { status: 200, body: paginate(all, req.path) };
1879
- }
1880
- if (seg[1] === 'threads' && seg.length === 5 && seg[3] === 'messages' && method === 'GET') {
1881
- const tid = dec(seg[2]!);
1882
- if (!getRow('thread', tid, req.root)) return notFound(`No thread found with id '${tid}'.`);
1883
- const m = getRow('message', dec(seg[4]!), req.root);
1884
- return m && m.thread_id === tid && !m._deleted ? { status: 200, body: msgView(m) } : notFound(`No message found with id '${dec(seg[4]!)}'.`);
1885
- }
1886
-
1887
- // ---- runs (+ run steps) (beta, stateful) ----
1888
- if (seg[1] === 'threads' && seg.length === 4 && seg[3] === 'runs' && method === 'POST') {
1889
- const tid = dec(seg[2]!);
1890
- if (!getRow('thread', tid, req.root)) return notFound(`No thread found with id '${tid}'.`);
1891
- return createRun(tid, params, req);
1892
- }
1893
- if (seg[1] === 'threads' && seg.length === 4 && seg[3] === 'runs' && method === 'GET') {
1894
- const tid = dec(seg[2]!);
1895
- if (!getRow('thread', tid, req.root)) return notFound(`No thread found with id '${tid}'.`);
1896
- const all = rows('run', req.root).filter((r) => r.thread_id === tid && !r._deleted).map(runView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1897
- return { status: 200, body: paginate(all, req.path) };
1898
- }
1899
- if (seg[1] === 'threads' && seg.length === 6 && seg[3] === 'runs' && seg[5] === 'steps' && method === 'GET') {
1900
- const tid = dec(seg[2]!);
1901
- const r = getRow('run', dec(seg[4]!), req.root);
1902
- if (!r || r.thread_id !== tid || r._deleted) return notFound(`No run found with id '${dec(seg[4]!)}'.`);
1903
- return { status: 200, body: { object: 'list', data: runSteps(r), has_more: false, first_id: `step-${r.id}-1`, last_id: `step-${r.id}-1` } };
1904
- }
1905
- if (seg[1] === 'threads' && seg.length === 7 && seg[3] === 'runs' && seg[5] === 'steps' && method === 'GET') {
1906
- const tid = dec(seg[2]!);
1907
- const r = getRow('run', dec(seg[4]!), req.root);
1908
- if (!r || r.thread_id !== tid || r._deleted) return notFound(`No run found with id '${dec(seg[4]!)}'.`);
1909
- const step = runSteps(r).find((s) => s.id === dec(seg[6]!));
1910
- return step ? { status: 200, body: step } : notFound(`No run step found with id '${dec(seg[6]!)}'.`);
1911
- }
1912
- if (seg[1] === 'threads' && seg.length === 5 && seg[3] === 'runs' && method === 'GET') {
1913
- const tid = dec(seg[2]!);
1914
- const r = getRow('run', dec(seg[4]!), req.root);
1915
- return r && r.thread_id === tid && !r._deleted ? { status: 200, body: runView(r) } : notFound(`No run found with id '${dec(seg[4]!)}'.`);
1916
- }
1917
- if (seg[1] === 'threads' && seg.length === 6 && seg[3] === 'runs' && seg[5] === 'cancel' && method === 'POST') {
1918
- const tid = dec(seg[2]!);
1919
- const r = getRow('run', dec(seg[4]!), req.root);
1920
- if (!r || r.thread_id !== tid || r._deleted) return notFound(`No run found with id '${dec(seg[4]!)}'.`);
1921
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'run.cancel', subjectType: 'run', subjectId: dec(seg[4]!), fields: { status: 'cancelled', cancelled_at: nowEpoch(req.occurredAt) }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1922
- return { status: 200, body: runView(resource) };
1923
- }
1924
-
1925
- // ---- admin: usage + costs reporting (computed over REAL recorded usage rows) ----
1926
- // The vendor's Usage/Costs API returns time-bucketed aggregates. The twin records a usage row per
1927
- // billable inference call (recordUsage) and aggregates them here — nothing is hardcoded; with no
1928
- // traffic the report is genuinely empty. A single bucket covers [0, now] (the twin is offline +
1929
- // time-frozen), grouped by model, faithful to the vendor's bucket/result shape.
1930
- if (seg[1] === 'organization' && seg[2] === 'usage' && (seg.length === 3 || seg.length === 4) && method === 'GET') {
1931
- const kindFilter = seg.length === 4 ? dec(seg[3]!) : undefined; // 'completions' | 'embeddings' | undefined (all)
1932
- const all = rows('usage_record', req.root).filter((r) => kindFilter === undefined || r.kind === kindFilter);
1933
- // group by model → one result object per model
1934
- const byModel = new Map<string, { input: number; output: number; requests: number }>();
1935
- for (const r of all) {
1936
- const m = String(r.model);
1937
- const agg = byModel.get(m) ?? { input: 0, output: 0, requests: 0 };
1938
- agg.input += Number(r.input_tokens ?? 0);
1939
- agg.output += Number(r.output_tokens ?? 0);
1940
- agg.requests += Number(r.num_model_requests ?? 0);
1941
- byModel.set(m, agg);
1177
+ if (typeof req.body === 'string' && method !== 'GET' && method !== 'HEAD' && !headers['content-type']) headers['content-type'] = 'application/json';
1178
+ const path = req.path.startsWith('/') ? req.path : `/${req.path}`;
1179
+ const response = await twin(new Request(`https://api.openai.com${path}`, { method, headers, ...(req.body !== undefined && method !== 'GET' && method !== 'HEAD' ? { body: req.body } : {}) }));
1180
+ const answerHeaders: Record<string, string> = {};
1181
+ response.headers.forEach((v, k) => { if (k !== 'content-type') answerHeaders[k] = v; });
1182
+ const type = response.headers.get('content-type') ?? '';
1183
+ const text = await response.text();
1184
+ if (type.includes('text/event-stream')) {
1185
+ // the frames back into events, for a caller that collects them
1186
+ for (const frame of text.split('\n\n')) {
1187
+ const data = frame.split('\n').filter((l) => l.startsWith('data: ')).map((l) => l.slice(6)).join('\n');
1188
+ if (!data) continue;
1189
+ req.sseSink?.(data === '[DONE]' ? { done: true } : { data: JSON.parse(data) as Record<string, unknown> });
1942
1190
  }
1943
- const resultObject = kindFilter === 'embeddings' ? 'organization.usage.embeddings.result' : 'organization.usage.completions.result';
1944
- const results = [...byModel.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([model, agg]) => ({
1945
- object: resultObject,
1946
- input_tokens: agg.input,
1947
- output_tokens: agg.output,
1948
- num_model_requests: agg.requests,
1949
- model,
1950
- project_id: null,
1951
- }));
1952
- const buckets = results.length === 0 ? [] : [{ object: 'bucket', start_time: 0, end_time: nowEpoch(req.occurredAt), results }];
1953
- return { status: 200, body: { object: 'page', data: buckets, has_more: false, next_page: null } };
1954
- }
1955
- if (path === '/v1/organization/costs' && method === 'GET') {
1956
- const all = rows('usage_record', req.root);
1957
- // group cost by line_item (the usage kind, vendor-style) → one result per line item
1958
- const byLine = new Map<string, number>();
1959
- for (const r of all) {
1960
- const line = String(r.kind);
1961
- byLine.set(line, (byLine.get(line) ?? 0) + Number(r.cost_usd ?? 0));
1962
- }
1963
- const results = [...byLine.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([line, amount]) => ({
1964
- object: 'organization.costs.result',
1965
- amount: { value: amount, currency: 'usd' },
1966
- line_item: line,
1967
- project_id: null,
1968
- }));
1969
- const buckets = results.length === 0 ? [] : [{ object: 'bucket', start_time: 0, end_time: nowEpoch(req.occurredAt), results }];
1970
- return { status: 200, body: { object: 'page', data: buckets, has_more: false, next_page: null } };
1971
- }
1972
-
1973
- // ---- admin: organization projects (stateful) ----
1974
- if (path === '/v1/organization/projects' && method === 'POST') return createProject(params, req);
1975
- if (path === '/v1/organization/projects' && method === 'GET') {
1976
- const includeArchived = new URLSearchParams(req.path.split('?')[1] ?? '').get('include_archived') === 'true';
1977
- const items = rows('project', req.root).filter((r) => includeArchived || r.status !== 'archived').map(idView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
1978
- return { status: 200, body: paginate(items, req.path) };
1979
- }
1980
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 4 && method === 'GET') {
1981
- const p = getRow('project', dec(seg[3]!), req.root);
1982
- return p ? { status: 200, body: idView(p) } : notFound(`Project ${dec(seg[3]!)} not found`);
1983
- }
1984
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 4 && method === 'POST') {
1985
- const pid = dec(seg[3]!);
1986
- if (!getRow('project', pid, req.root)) return notFound(`Project ${pid} not found`);
1987
- const fields = pickUpdate(params, ['name']);
1988
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'project.update', subjectType: 'project', subjectId: pid, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1989
- return { status: 200, body: idView(resource) };
1191
+ return { status: response.status, body: null, headers: answerHeaders };
1990
1192
  }
1991
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 5 && seg[4] === 'archive' && method === 'POST') {
1992
- const pid = dec(seg[3]!);
1993
- if (!getRow('project', pid, req.root)) return notFound(`Project ${pid} not found`);
1994
- const { resource } = await applyTwinWrite(SERVICE, { operation: 'project.archive', subjectType: 'project', subjectId: pid, fields: { status: 'archived', archived_at: nowEpoch(req.occurredAt) }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
1995
- return { status: 200, body: idView(resource) };
1996
- }
1997
- // ---- admin: project API keys (stateful; synthetic twin keys) ----
1998
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 5 && seg[4] === 'api_keys' && method === 'POST') {
1999
- const pid = dec(seg[3]!);
2000
- if (!getRow('project', pid, req.root)) return notFound(`Project ${pid} not found`);
2001
- return createProjectApiKey(pid, params, req);
2002
- }
2003
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 5 && seg[4] === 'api_keys' && method === 'GET') {
2004
- const pid = dec(seg[3]!);
2005
- if (!getRow('project', pid, req.root)) return notFound(`Project ${pid} not found`);
2006
- const items = rows('api_key', req.root).filter((r) => r.project_id === pid && !r._deleted).map(apiKeyView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
2007
- return { status: 200, body: paginate(items, req.path) };
2008
- }
2009
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 6 && seg[4] === 'api_keys' && method === 'GET') {
2010
- const k = getRow('api_key', dec(seg[5]!), req.root);
2011
- return k && k.project_id === dec(seg[3]!) && !k._deleted ? { status: 200, body: apiKeyView(k) } : notFound(`API key ${dec(seg[5]!)} not found`);
2012
- }
2013
- if (seg[1] === 'organization' && seg[2] === 'projects' && seg.length === 6 && seg[4] === 'api_keys' && method === 'DELETE') {
2014
- const kid = dec(seg[5]!);
2015
- const k = getRow('api_key', kid, req.root);
2016
- if (!k || k.project_id !== dec(seg[3]!) || k._deleted) return notFound(`API key ${kid} not found`);
2017
- await applyTwinWrite(SERVICE, { operation: 'api_key.delete', subjectType: 'api_key', subjectId: kid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
2018
- return { status: 200, body: { id: kid, object: 'organization.project.api_key.deleted', deleted: true } };
2019
- }
2020
-
2021
- // ---- evals (stateful: eval config → runs → output items) ----
2022
- if (path === '/v1/evals' && method === 'POST') return createEval(params, req);
2023
- if (path === '/v1/evals' && method === 'GET') {
2024
- const items = rows('eval', req.root).map(evalView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
2025
- return { status: 200, body: paginate(items, req.path) };
2026
- }
2027
- if (seg[1] === 'evals' && seg.length === 3 && method === 'GET') {
2028
- const e = getRow('eval', dec(seg[2]!), req.root);
2029
- return e ? { status: 200, body: evalView(e) } : notFound(`Eval ${dec(seg[2]!)} not found`);
2030
- }
2031
- if (seg[1] === 'evals' && seg.length === 4 && seg[3] === 'runs' && method === 'POST') {
2032
- const evalId = dec(seg[2]!);
2033
- if (!getRow('eval', evalId, req.root)) return notFound(`Eval ${evalId} not found`);
2034
- return createEvalRun(evalId, params, req);
2035
- }
2036
- if (seg[1] === 'evals' && seg.length === 4 && seg[3] === 'runs' && method === 'GET') {
2037
- const evalId = dec(seg[2]!);
2038
- if (!getRow('eval', evalId, req.root)) return notFound(`Eval ${evalId} not found`);
2039
- const items = rows('eval_run', req.root).filter((r) => r.eval_id === evalId).map(evalRunView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
2040
- return { status: 200, body: paginate(items, req.path) };
2041
- }
2042
- if (seg[1] === 'evals' && seg.length === 5 && seg[3] === 'runs' && method === 'GET') {
2043
- const run = getRow('eval_run', dec(seg[4]!), req.root);
2044
- return run && run.eval_id === dec(seg[2]!) ? { status: 200, body: evalRunView(run) } : notFound(`Eval run ${dec(seg[4]!)} not found`);
2045
- }
2046
- if (seg[1] === 'evals' && seg.length === 6 && seg[3] === 'runs' && seg[5] === 'output_items' && method === 'GET') {
2047
- const run = getRow('eval_run', dec(seg[4]!), req.root);
2048
- if (!run || run.eval_id !== dec(seg[2]!)) return notFound(`Eval run ${dec(seg[4]!)} not found`);
2049
- return { status: 200, body: paginate(evalRunOutputItems(run), req.path) };
2050
- }
2051
-
2052
- // ---- containers (code-interpreter sandboxes) → container files ----
2053
- if (path === '/v1/containers' && method === 'POST') return createContainer(params, req);
2054
- if (path === '/v1/containers' && method === 'GET') {
2055
- const items = rows('container', req.root).filter((r) => !r._deleted).map(containerView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
2056
- return { status: 200, body: paginate(items, req.path) };
2057
- }
2058
- if (seg[1] === 'containers' && seg.length === 3 && method === 'GET') {
2059
- const c = getRow('container', dec(seg[2]!), req.root);
2060
- return c && !c._deleted ? { status: 200, body: containerView(c) } : notFound(`Container ${dec(seg[2]!)} not found`);
2061
- }
2062
- if (seg[1] === 'containers' && seg.length === 3 && method === 'DELETE') {
2063
- const cid = dec(seg[2]!);
2064
- const c = getRow('container', cid, req.root);
2065
- if (!c || c._deleted) return notFound(`Container ${cid} not found`);
2066
- await applyTwinWrite(SERVICE, { operation: 'container.delete', subjectType: 'container', subjectId: cid, fields: { _deleted: true, status: 'deleted' }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
2067
- return { status: 200, body: { id: cid, object: 'container.deleted', deleted: true } };
2068
- }
2069
- if (seg[1] === 'containers' && seg.length === 4 && seg[3] === 'files' && method === 'POST') {
2070
- const cid = dec(seg[2]!);
2071
- const c = getRow('container', cid, req.root);
2072
- if (!c || c._deleted) return notFound(`Container ${cid} not found`);
2073
- return createContainerFile(cid, params, req);
2074
- }
2075
- if (seg[1] === 'containers' && seg.length === 4 && seg[3] === 'files' && method === 'GET') {
2076
- const cid = dec(seg[2]!);
2077
- if (!getRow('container', cid, req.root)) return notFound(`Container ${cid} not found`);
2078
- const items = rows('container_file', req.root).filter((r) => r.container_id === cid && !r._deleted).map(containerFileView).sort((a, b) => Number(b.created_at) - Number(a.created_at));
2079
- return { status: 200, body: paginate(items, req.path) };
2080
- }
2081
- if (seg[1] === 'containers' && seg.length === 5 && seg[3] === 'files' && method === 'GET') {
2082
- const cf = getRow('container_file', dec(seg[4]!), req.root);
2083
- return cf && cf.container_id === dec(seg[2]!) && !cf._deleted ? { status: 200, body: containerFileView(cf) } : notFound(`Container file ${dec(seg[4]!)} not found`);
2084
- }
2085
- if (seg[1] === 'containers' && seg.length === 6 && seg[3] === 'files' && seg[5] === 'content' && method === 'GET') {
2086
- const cf = getRow('container_file', dec(seg[4]!), req.root);
2087
- if (!cf || cf.container_id !== dec(seg[2]!) || cf._deleted) return notFound(`Container file ${dec(seg[4]!)} not found`);
2088
- return { status: 200, body: (cf._content as string) ?? '' };
2089
- }
2090
- if (seg[1] === 'containers' && seg.length === 5 && seg[3] === 'files' && method === 'DELETE') {
2091
- const fid = dec(seg[4]!);
2092
- const cf = getRow('container_file', fid, req.root);
2093
- if (!cf || cf.container_id !== dec(seg[2]!) || cf._deleted) return notFound(`Container file ${fid} not found`);
2094
- await applyTwinWrite(SERVICE, { operation: 'container_file.delete', subjectType: 'container_file', subjectId: fid, fields: { _deleted: true }, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
2095
- return { status: 200, body: { id: fid, object: 'container.file.deleted', deleted: true } };
2096
- }
2097
-
2098
- // Unknown route → vendor-faithful 404 (never a fabricated success — D2).
2099
- return notFound(`Unknown request URL: ${method} ${path}. Please check the URL for typos.`);
1193
+ const body = type.includes('json') ? (text ? JSON.parse(text) : null) : text;
1194
+ return { status: response.status, body, headers: answerHeaders };
2100
1195
  }