@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1,901 @@
1
+ /**
2
+ * OpenAI Responses emulator core (internal; not a package export).
3
+ *
4
+ * One `POST .../responses` route with request parsing (`model`, `input`, `instructions`, `tools`,
5
+ * `tool_choice`, `reasoning`, `stream`, `store`, `max_output_tokens`), Responses SSE with typed
6
+ * `event:` names in the order the API sends them, the non-streamed `response` JSON body, scripted
7
+ * turns, and a JSON error envelope chosen by each subpath. Faults, the request ledger, the
8
+ * `/_emulate/*` control plane, evidence tagging, and route binding come from the shared kernel
9
+ * (`emulator-kernel.ts`).
10
+ *
11
+ * Streamed responses: `response.created` and `response.in_progress`, then per output item
12
+ * `response.output_item.added`, its parts and deltas, and `response.output_item.done`, then
13
+ * `response.completed` with the full `response` (output items and usage). Output items:
14
+ * `reasoning` (one summary part: `response.reasoning_summary_part.added`,
15
+ * `response.reasoning_summary_text.delta` / `.done`, `response.reasoning_summary_part.done`) when
16
+ * the request asks for a reasoning summary, `message` (`response.content_part.added`,
17
+ * `response.output_text.delta` / `.done`, `response.content_part.done`), and `function_call`
18
+ * (`response.function_call_arguments.delta` / `.done`). Every event carries a `sequence_number`.
19
+ *
20
+ * Each subpath supplies its path and manifest, model list, error envelope and unknown-model
21
+ * status, the 401 error for a missing bearer credential, extra header rules (required headers
22
+ * with their status and error, and which non-credential header values the ledger records),
23
+ * whether `max_output_tokens` is accepted, and its input-invalid error. Bearer values are never
24
+ * checked or stored.
25
+ *
26
+ * Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`, `TextEncoder`, `URL`);
27
+ * no Node builtins and no SDK imports.
28
+ */
29
+ import { Predicate, Result } from 'effect'
30
+ import * as Schema from 'effect/Schema'
31
+ import {
32
+ approximateTokens,
33
+ ChunkCount,
34
+ controlError,
35
+ EmulatorErrorAfterChunksFault,
36
+ emulatorFaultFields,
37
+ EmulatorScriptedError,
38
+ EmulatorStatusFault,
39
+ EmulatorTruncateAfterChunksFault,
40
+ isJsonObject,
41
+ jsonField,
42
+ jsonResponse,
43
+ makeEmulatorKernel,
44
+ parseJson,
45
+ readText,
46
+ splitFragments,
47
+ stringField,
48
+ synthesizeValue,
49
+ type EmulatorApi,
50
+ type KernelLedgerEntry
51
+ } from './emulator-kernel.ts'
52
+ import {
53
+ emulatorRouteKey,
54
+ type EmulatorEvidence,
55
+ type EmulatorRouteEvidence
56
+ } from './route-evidence.ts'
57
+
58
+ /** A mid-stream Responses error: a machine `code` and a message. */
59
+ export const ResponsesStreamError = Schema.Struct({
60
+ code: Schema.NonEmptyString,
61
+ message: Schema.String
62
+ })
63
+
64
+ export type ResponsesStreamError = typeof ResponsesStreamError.Type
65
+
66
+ /**
67
+ * Send `chunks` SSE events, then one error event and close without `response.completed`:
68
+ * `event: 'error'` (the default) sends `{ type: 'error', code, message }`, and
69
+ * `event: 'response.failed'` sends a `response.failed` event whose `response.error` carries the
70
+ * code and message. The error defaults to `server_error`. Streamed responses only; it must come
71
+ * before `response.completed`.
72
+ */
73
+ export const ResponsesErrorEventFault = Schema.Struct({
74
+ kind: Schema.Literal('error-event-after-chunks'),
75
+ chunks: ChunkCount,
76
+ event: Schema.optionalKey(Schema.Literals(['error', 'response.failed'])),
77
+ error: Schema.optionalKey(ResponsesStreamError),
78
+ ...emulatorFaultFields
79
+ })
80
+
81
+ /**
82
+ * Wire faults: `status` (the body defaults to the subpath's error envelope for the status),
83
+ * `error-after-chunks` (a dropped connection), `truncate-after-chunks` (a clean close, for example
84
+ * before `response.completed`), and `error-event-after-chunks` (a mid-stream `error` or
85
+ * `response.failed` event).
86
+ */
87
+ export const ResponsesFault = Schema.Union([
88
+ EmulatorStatusFault,
89
+ EmulatorErrorAfterChunksFault,
90
+ EmulatorTruncateAfterChunksFault,
91
+ ResponsesErrorEventFault
92
+ ])
93
+
94
+ export type ResponsesFault = typeof ResponsesFault.Type
95
+
96
+ export type ResponsesFaultKind = ResponsesFault['kind']
97
+
98
+ const TokenCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
99
+
100
+ /** Usage for a scripted turn (sent in `response.completed` or the JSON body). */
101
+ export const ResponsesScriptedUsage = Schema.Struct({
102
+ inputTokens: TokenCount,
103
+ outputTokens: TokenCount,
104
+ reasoningTokens: Schema.optionalKey(TokenCount)
105
+ })
106
+
107
+ export type ResponsesScriptedUsage = typeof ResponsesScriptedUsage.Type
108
+
109
+ /** One scripted `function_call` item; its arguments stream as these fragments. */
110
+ export const ResponsesScriptedFunctionCall = Schema.Struct({
111
+ name: Schema.NonEmptyString,
112
+ argumentFragments: Schema.Array(Schema.String),
113
+ callId: Schema.optionalKey(Schema.NonEmptyString)
114
+ })
115
+
116
+ export type ResponsesScriptedFunctionCall = typeof ResponsesScriptedFunctionCall.Type
117
+
118
+ /**
119
+ * A scripted response. Every field is exact (nothing is filled in) except: an item is sent only
120
+ * when its field is present (`reasoning` is one summary part, `text` one message); `usage`
121
+ * omitted is synthesized and `null` drops `usage` from the wire; `order` defaults to
122
+ * `reasoning-first` (`text-first` sends the reasoning item after the message); `format` defaults
123
+ * to the request's `stream` (`sse` streams even for `stream: false`, `json` answers one JSON body
124
+ * even for `stream: true`, as the JSON fallback some endpoints use).
125
+ */
126
+ export const ResponsesScriptedResponse = Schema.Struct({
127
+ reasoning: Schema.optionalKey(Schema.Array(Schema.String)),
128
+ text: Schema.optionalKey(Schema.Array(Schema.String)),
129
+ functionCalls: Schema.optionalKey(Schema.Array(ResponsesScriptedFunctionCall)),
130
+ order: Schema.optionalKey(Schema.Literals(['reasoning-first', 'text-first'])),
131
+ usage: Schema.optionalKey(Schema.NullOr(ResponsesScriptedUsage)),
132
+ format: Schema.optionalKey(Schema.Literals(['sse', 'json']))
133
+ })
134
+
135
+ export type ResponsesScriptedResponse = typeof ResponsesScriptedResponse.Type
136
+
137
+ /** A turn queued for the next Responses request. */
138
+ export const ResponsesScriptedTurn = Schema.Union([
139
+ EmulatorScriptedError,
140
+ ResponsesScriptedResponse
141
+ ])
142
+
143
+ export type ResponsesScriptedTurn = typeof ResponsesScriptedTurn.Type
144
+
145
+ export type ResponsesLedgerEntry = {
146
+ /** 1-based arrival order since the last ledger clear or reset. */
147
+ readonly seq: number
148
+ readonly method: string
149
+ readonly path: string
150
+ /** Parsed JSON request body, when it was valid JSON. */
151
+ readonly body?: Schema.Json
152
+ readonly model?: string
153
+ readonly stream?: boolean
154
+ readonly store?: boolean
155
+ /** The request's `max_output_tokens`, when it is a number. */
156
+ readonly maxOutputTokens?: number
157
+ readonly reasoning?: Schema.Json
158
+ readonly toolChoice?: Schema.Json
159
+ /** Names of the offered `function` tools. */
160
+ readonly toolNames: ReadonlyArray<string>
161
+ /** Values of the non-credential headers the subpath records (lower-case names). */
162
+ readonly headers: Readonly<Record<string, string>>
163
+ /** Kind of the fault that shaped the response. */
164
+ readonly fault?: ResponsesFaultKind
165
+ /** Why a matching fault could not take effect (the response was a 500 emulator error). */
166
+ readonly faultError?: string
167
+ /**
168
+ * Set when the emulator could not build the planned response; the request was answered with a
169
+ * 500 emulator error (still evidence-tagged) and no fault was used up.
170
+ */
171
+ readonly responseError?: string
172
+ /** Set when a scripted turn answered the request. */
173
+ readonly scripted?: 'response' | 'error'
174
+ /** Evidence of the matched route; `unknown-route` for requests that failed closed. */
175
+ readonly evidence: EmulatorEvidence | 'unknown-route'
176
+ readonly status: number
177
+ /** Body chunks handed to the transport so far (each produced when pulled). */
178
+ readonly bodyChunks: number
179
+ }
180
+
181
+ export type ResponsesEmulator = EmulatorApi<
182
+ ResponsesScriptedTurn,
183
+ ResponsesFault,
184
+ ResponsesLedgerEntry
185
+ >
186
+
187
+ /** A Responses wire error, rendered by the subpath's `errorEnvelope`. */
188
+ export type ResponsesWireError = {
189
+ readonly message: string
190
+ readonly type: string
191
+ readonly code: string
192
+ readonly param?: string
193
+ }
194
+
195
+ /**
196
+ * One request header rule. `record` keeps the header value in the ledger `headers` (never set it
197
+ * for credential headers); `required` answers a request without a non-empty value with that
198
+ * status and error (the value itself is never checked).
199
+ */
200
+ export type ResponsesHeaderRule = {
201
+ /** Lower-case header name. */
202
+ readonly name: string
203
+ readonly record: boolean
204
+ readonly required?: { readonly status: number; readonly error: ResponsesWireError }
205
+ }
206
+
207
+ /**
208
+ * What differs between Responses emulators. The core owns everything else (framing, faults,
209
+ * scripting, ledger, control plane, evidence).
210
+ */
211
+ export type ResponsesEmulatorConfig = {
212
+ /** The Responses path, for example `/v1/responses`. */
213
+ readonly path: string
214
+ /** Route evidence manifest; must list exactly the Responses route. */
215
+ readonly routes: ReadonlyArray<EmulatorRouteEvidence>
216
+ readonly knownModels: ReadonlyArray<string>
217
+ /** Renders a wire error as the service's JSON error envelope. */
218
+ readonly errorEnvelope: (error: ResponsesWireError) => Schema.Json
219
+ /** Status and error for an unknown model id. */
220
+ readonly unknownModel: { readonly status: number; readonly error: ResponsesWireError }
221
+ /**
222
+ * Requests authenticate with a non-empty `Authorization: Bearer` credential, never checked or
223
+ * stored; anything else answers 401 with `unauthorized`.
224
+ */
225
+ readonly unauthorized: ResponsesWireError
226
+ /** Extra header rules, checked in order after the bearer credential. */
227
+ readonly headers: ReadonlyArray<ResponsesHeaderRule>
228
+ /**
229
+ * `optional`: `max_output_tokens` may be sent (a positive integer, or 400) and is recorded, not
230
+ * enforced; `rejected`: any `max_output_tokens` answers 400 `unsupported_parameter`.
231
+ */
232
+ readonly outputTokenLimit: 'optional' | 'rejected'
233
+ /** Default answer text deltas. */
234
+ readonly defaultText: ReadonlyArray<string>
235
+ /** Builds the error thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input. */
236
+ readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error
237
+ }
238
+
239
+ const ResponsesTool = Schema.Struct({
240
+ type: Schema.optionalKey(Schema.String),
241
+ name: Schema.optionalKey(Schema.String),
242
+ description: Schema.optionalKey(Schema.NullOr(Schema.String)),
243
+ parameters: Schema.optionalKey(Schema.Json)
244
+ })
245
+
246
+ const ResponsesRequest = Schema.Struct({
247
+ model: Schema.optionalKey(Schema.String),
248
+ input: Schema.Union([Schema.String, Schema.Array(Schema.Json)]),
249
+ instructions: Schema.optionalKey(Schema.NullOr(Schema.String)),
250
+ tools: Schema.optionalKey(Schema.NullOr(Schema.Array(ResponsesTool))),
251
+ tool_choice: Schema.optionalKey(Schema.Json),
252
+ reasoning: Schema.optionalKey(Schema.Json),
253
+ stream: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
254
+ store: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
255
+ max_output_tokens: Schema.optionalKey(Schema.Json)
256
+ })
257
+
258
+ type ResponsesRequest = typeof ResponsesRequest.Type
259
+
260
+ const decodeResponsesRequest = Schema.decodeUnknownResult(ResponsesRequest)
261
+
262
+ // Synthetic creation time shared by every emulated response (deterministic output).
263
+ const syntheticCreated = 1790000000
264
+
265
+ // A non-empty bearer credential. The value is never checked or stored.
266
+ const bearerPattern = /^bearer\s+\S+/i
267
+
268
+ const defaultReasoning = ['The user wants a short reply.', ' Keep it brief.']
269
+
270
+ const defaultStreamError: ResponsesStreamError = {
271
+ code: 'server_error',
272
+ message: 'Synthetic: the server had an error while processing the request.'
273
+ }
274
+
275
+ type ItemPlan =
276
+ | { readonly type: 'reasoning'; readonly id: string; readonly fragments: ReadonlyArray<string> }
277
+ | { readonly type: 'message'; readonly id: string; readonly fragments: ReadonlyArray<string> }
278
+ | {
279
+ readonly type: 'function_call'
280
+ readonly id: string
281
+ readonly callId: string
282
+ readonly name: string
283
+ readonly fragments: ReadonlyArray<string>
284
+ }
285
+
286
+ type ResponsePlan = {
287
+ readonly items: ReadonlyArray<ItemPlan>
288
+ /** `null`: never send usage; `undefined`: synthesize. */
289
+ readonly usage: ResponsesScriptedUsage | null | undefined
290
+ /** `undefined`: follow the request's `stream`. */
291
+ readonly format: 'sse' | 'json' | undefined
292
+ }
293
+
294
+ /** A reasoning summary is streamed when the request's `reasoning` asks for one. */
295
+ const summaryRequested = (request: ResponsesRequest): boolean => {
296
+ const summary = stringField(request.reasoning, 'summary')
297
+
298
+ return summary !== undefined && summary.length > 0
299
+ }
300
+
301
+ const functionTools = (request: ResponsesRequest) =>
302
+ (request.tools ?? []).flatMap(tool =>
303
+ (tool.type === undefined || tool.type === 'function') && tool.name !== undefined
304
+ ? [{ name: tool.name, parameters: tool.parameters }]
305
+ : []
306
+ )
307
+
308
+ const chosenTool = (request: ResponsesRequest) => {
309
+ const tools = functionTools(request)
310
+
311
+ if (tools.length === 0 || request.tool_choice === 'none') {
312
+ return undefined
313
+ }
314
+
315
+ const forced =
316
+ stringField(request.tool_choice, 'type') === 'function'
317
+ ? stringField(request.tool_choice, 'name')
318
+ : undefined
319
+
320
+ return tools.find(tool => tool.name === forced) ?? tools[0]
321
+ }
322
+
323
+ const defaultPlan = (
324
+ request: ResponsesRequest,
325
+ seq: number,
326
+ defaultText: ReadonlyArray<string>
327
+ ): ResponsePlan => {
328
+ const reasoning: ReadonlyArray<ItemPlan> = summaryRequested(request)
329
+ ? [{ type: 'reasoning', id: `rs_synthetic_${seq}_0`, fragments: defaultReasoning }]
330
+ : []
331
+
332
+ const tool = chosenTool(request)
333
+
334
+ if (tool === undefined) {
335
+ return {
336
+ items: [
337
+ ...reasoning,
338
+ { type: 'message', id: `msg_synthetic_${seq}_0`, fragments: defaultText }
339
+ ],
340
+ usage: undefined,
341
+ format: undefined
342
+ }
343
+ }
344
+
345
+ const argumentsText = JSON.stringify(
346
+ synthesizeValue(tool.parameters ?? { type: 'object' }, tool.name, 0)
347
+ )
348
+
349
+ return {
350
+ items: [
351
+ ...reasoning,
352
+ {
353
+ type: 'function_call',
354
+ id: `fc_synthetic_${seq}_0`,
355
+ callId: `call_synthetic_${seq}_0`,
356
+ name: tool.name,
357
+ fragments: splitFragments(argumentsText, 3)
358
+ }
359
+ ],
360
+ usage: undefined,
361
+ format: undefined
362
+ }
363
+ }
364
+
365
+ const scriptedPlan = (turn: ResponsesScriptedResponse, seq: number): ResponsePlan => {
366
+ const reasoning: ReadonlyArray<ItemPlan> =
367
+ turn.reasoning === undefined
368
+ ? []
369
+ : [{ type: 'reasoning', id: `rs_synthetic_${seq}_0`, fragments: turn.reasoning }]
370
+
371
+ const message: ReadonlyArray<ItemPlan> =
372
+ turn.text === undefined
373
+ ? []
374
+ : [{ type: 'message', id: `msg_synthetic_${seq}_0`, fragments: turn.text }]
375
+
376
+ const calls = (turn.functionCalls ?? []).map((call, index): ItemPlan => ({
377
+ type: 'function_call',
378
+ id: `fc_synthetic_${seq}_${index}`,
379
+ callId: call.callId ?? `call_synthetic_${seq}_${index}`,
380
+ name: call.name,
381
+ fragments: call.argumentFragments
382
+ }))
383
+
384
+ return {
385
+ items:
386
+ turn.order === 'text-first'
387
+ ? [...message, ...reasoning, ...calls]
388
+ : [...reasoning, ...message, ...calls],
389
+ usage: turn.usage,
390
+ format: turn.format
391
+ }
392
+ }
393
+
394
+ type WireUsage = {
395
+ readonly input_tokens: number
396
+ readonly input_tokens_details: { readonly cached_tokens: number }
397
+ readonly output_tokens: number
398
+ readonly output_tokens_details: { readonly reasoning_tokens: number }
399
+ readonly total_tokens: number
400
+ }
401
+
402
+ type WireResponse = {
403
+ readonly id: string
404
+ readonly object: 'response'
405
+ readonly created_at: number
406
+ readonly status: 'in_progress' | 'completed' | 'failed'
407
+ readonly error: Schema.JsonObject | null
408
+ readonly incomplete_details: null
409
+ readonly model: string
410
+ readonly output: ReadonlyArray<Schema.JsonObject>
411
+ usage?: WireUsage | null
412
+ }
413
+
414
+ const planUsage = (plan: ResponsePlan, requestText: string): WireUsage | undefined => {
415
+ if (plan.usage === null) {
416
+ return undefined
417
+ }
418
+
419
+ const reasoningText = plan.items
420
+ .flatMap(item => (item.type === 'reasoning' ? item.fragments : []))
421
+ .join('')
422
+
423
+ const usage = plan.usage ?? {
424
+ inputTokens: approximateTokens(requestText),
425
+ outputTokens: approximateTokens(plan.items.flatMap(item => item.fragments).join('')),
426
+ reasoningTokens: reasoningText.length > 0 ? approximateTokens(reasoningText) : 0
427
+ }
428
+
429
+ return {
430
+ input_tokens: usage.inputTokens,
431
+ input_tokens_details: { cached_tokens: 0 },
432
+ output_tokens: usage.outputTokens,
433
+ output_tokens_details: { reasoning_tokens: usage.reasoningTokens ?? 0 },
434
+ total_tokens: usage.inputTokens + usage.outputTokens
435
+ }
436
+ }
437
+
438
+ type ResponseIdentity = { readonly id: string; readonly model: string }
439
+
440
+ const responseObject = (
441
+ identity: ResponseIdentity,
442
+ status: WireResponse['status'],
443
+ output: ReadonlyArray<Schema.JsonObject>
444
+ ): WireResponse => ({
445
+ id: identity.id,
446
+ object: 'response',
447
+ created_at: syntheticCreated,
448
+ status,
449
+ error: null,
450
+ incomplete_details: null,
451
+ model: identity.model,
452
+ output
453
+ })
454
+
455
+ const outputText = (text: string): Schema.JsonObject => ({
456
+ type: 'output_text',
457
+ text,
458
+ annotations: []
459
+ })
460
+
461
+ const summaryText = (text: string): Schema.JsonObject => ({ type: 'summary_text', text })
462
+
463
+ /** The complete wire item (as in `response.output_item.done` and the final `output`). */
464
+ const completeItem = (item: ItemPlan): Schema.JsonObject => {
465
+ const text = item.fragments.join('')
466
+
467
+ switch (item.type) {
468
+ case 'reasoning':
469
+ return {
470
+ id: item.id,
471
+ type: 'reasoning',
472
+ summary: item.fragments.length === 0 ? [] : [summaryText(text)]
473
+ }
474
+ case 'message':
475
+ return {
476
+ id: item.id,
477
+ type: 'message',
478
+ status: 'completed',
479
+ role: 'assistant',
480
+ content: [outputText(text)]
481
+ }
482
+ case 'function_call':
483
+ return {
484
+ id: item.id,
485
+ type: 'function_call',
486
+ status: 'completed',
487
+ call_id: item.callId,
488
+ name: item.name,
489
+ arguments: text
490
+ }
491
+ }
492
+ }
493
+
494
+ /** The item as first announced in `response.output_item.added`. */
495
+ const addedItem = (item: ItemPlan): Schema.JsonObject => {
496
+ switch (item.type) {
497
+ case 'reasoning':
498
+ return { id: item.id, type: 'reasoning', summary: [] }
499
+ case 'message':
500
+ return {
501
+ id: item.id,
502
+ type: 'message',
503
+ status: 'in_progress',
504
+ role: 'assistant',
505
+ content: []
506
+ }
507
+ case 'function_call':
508
+ return {
509
+ id: item.id,
510
+ type: 'function_call',
511
+ status: 'in_progress',
512
+ call_id: item.callId,
513
+ name: item.name,
514
+ arguments: ''
515
+ }
516
+ }
517
+ }
518
+
519
+ const sseEvent = (type: string, sequence: number, payload: object): string =>
520
+ `event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: sequence, ...payload })}\n\n`
521
+
522
+ type EventWriter = {
523
+ readonly events: Array<string>
524
+ readonly push: (type: string, payload: object) => void
525
+ }
526
+
527
+ /** Builds SSE events with increasing `sequence_number`s, one network chunk each. */
528
+ const makeEventWriter = (): EventWriter => {
529
+ const events: Array<string> = []
530
+
531
+ return {
532
+ events,
533
+ push: (type, payload) => {
534
+ events.push(sseEvent(type, events.length, payload))
535
+ }
536
+ }
537
+ }
538
+
539
+ const itemEvents = (writer: EventWriter, item: ItemPlan, outputIndex: number) => {
540
+ const text = item.fragments.join('')
541
+ const ids = { item_id: item.id, output_index: outputIndex }
542
+
543
+ writer.push('response.output_item.added', { output_index: outputIndex, item: addedItem(item) })
544
+
545
+ switch (item.type) {
546
+ case 'reasoning': {
547
+ if (item.fragments.length > 0) {
548
+ const part = { ...ids, summary_index: 0 }
549
+
550
+ writer.push('response.reasoning_summary_part.added', { ...part, part: summaryText('') })
551
+
552
+ for (const fragment of item.fragments) {
553
+ writer.push('response.reasoning_summary_text.delta', { ...part, delta: fragment })
554
+ }
555
+
556
+ writer.push('response.reasoning_summary_text.done', { ...part, text })
557
+ writer.push('response.reasoning_summary_part.done', { ...part, part: summaryText(text) })
558
+ }
559
+
560
+ break
561
+ }
562
+
563
+ case 'message': {
564
+ const part = { ...ids, content_index: 0 }
565
+
566
+ writer.push('response.content_part.added', { ...part, part: outputText('') })
567
+
568
+ for (const fragment of item.fragments) {
569
+ writer.push('response.output_text.delta', { ...part, delta: fragment })
570
+ }
571
+
572
+ writer.push('response.output_text.done', { ...part, text })
573
+ writer.push('response.content_part.done', { ...part, part: outputText(text) })
574
+ break
575
+ }
576
+
577
+ case 'function_call': {
578
+ for (const fragment of item.fragments) {
579
+ writer.push('response.function_call_arguments.delta', { ...ids, delta: fragment })
580
+ }
581
+
582
+ writer.push('response.function_call_arguments.done', { ...ids, arguments: text })
583
+ break
584
+ }
585
+ }
586
+
587
+ writer.push('response.output_item.done', { output_index: outputIndex, item: completeItem(item) })
588
+ }
589
+
590
+ /** The completed `response` object (for `response.completed` and the JSON body). */
591
+ const completedResponse = (
592
+ plan: ResponsePlan,
593
+ identity: ResponseIdentity,
594
+ usage: WireUsage | undefined
595
+ ): WireResponse => {
596
+ const response = responseObject(identity, 'completed', plan.items.map(completeItem))
597
+
598
+ if (usage !== undefined) {
599
+ response.usage = usage
600
+ }
601
+
602
+ return response
603
+ }
604
+
605
+ /** SSE events for a streamed response, one network chunk each, in the API's order. */
606
+ const streamEvents = (
607
+ plan: ResponsePlan,
608
+ identity: ResponseIdentity,
609
+ usage: WireUsage | undefined
610
+ ): ReadonlyArray<string> => {
611
+ const writer = makeEventWriter()
612
+ const inProgress: WireResponse = { ...responseObject(identity, 'in_progress', []), usage: null }
613
+
614
+ writer.push('response.created', { response: inProgress })
615
+ writer.push('response.in_progress', { response: inProgress })
616
+
617
+ plan.items.forEach((item, index) => itemEvents(writer, item, index))
618
+
619
+ writer.push('response.completed', { response: completedResponse(plan, identity, usage) })
620
+
621
+ return writer.events
622
+ }
623
+
624
+ /** The mid-stream error event of an `error-event-after-chunks` fault. */
625
+ const errorEvent = (
626
+ fault: typeof ResponsesErrorEventFault.Type,
627
+ identity: ResponseIdentity,
628
+ sequence: number
629
+ ): string => {
630
+ const error = fault.error ?? defaultStreamError
631
+
632
+ if (fault.event === 'response.failed') {
633
+ return sseEvent('response.failed', sequence, {
634
+ response: {
635
+ ...responseObject(identity, 'failed', []),
636
+ error: { code: error.code, message: error.message },
637
+ usage: null
638
+ }
639
+ })
640
+ }
641
+
642
+ return sseEvent('error', sequence, { code: error.code, message: error.message, param: null })
643
+ }
644
+
645
+ type MutableLedgerEntry = KernelLedgerEntry<ResponsesFaultKind> & {
646
+ body?: Schema.Json
647
+ model?: string
648
+ stream?: boolean
649
+ store?: boolean
650
+ maxOutputTokens?: number
651
+ reasoning?: Schema.Json
652
+ toolChoice?: Schema.Json
653
+ toolNames: ReadonlyArray<string>
654
+ headers: Record<string, string>
655
+ scripted?: 'response' | 'error'
656
+ }
657
+
658
+ const isPositiveInteger = (value: Schema.Json | undefined): value is number =>
659
+ Predicate.isNumber(value) && Number.isSafeInteger(value) && value > 0
660
+
661
+ /**
662
+ * Create a Responses emulator from its config. Each call has independent ledger, fault, and
663
+ * script state.
664
+ *
665
+ * Precedence per request: the bearer credential, the header rules in order, JSON and request
666
+ * validation (an `input` string or array, and the `max_output_tokens` policy), then the first
667
+ * matching fault if it is a `status` fault, then the next scripted turn, then model validation and
668
+ * defaults; a first matching body fault then shapes the body. Only the first matching fault (in
669
+ * insertion order) applies. Credential headers are never recorded, and credential values are
670
+ * never checked or stored.
671
+ */
672
+ export const makeResponsesEmulator = (config: ResponsesEmulatorConfig): ResponsesEmulator => {
673
+ const knownModels = [...config.knownModels]
674
+
675
+ const errorResponse = (status: number, error: ResponsesWireError): Response =>
676
+ jsonResponse(status, config.errorEnvelope(error))
677
+
678
+ const invalidRequest = (message: string, code: string, param?: string): Response =>
679
+ errorResponse(
680
+ 400,
681
+ param === undefined
682
+ ? { message, type: 'invalid_request_error', code }
683
+ : { message, type: 'invalid_request_error', code, param }
684
+ )
685
+
686
+ const defaultFaultBody = (status: number): Schema.Json =>
687
+ status === 429
688
+ ? config.errorEnvelope({
689
+ message: 'Synthetic rate limit: too many requests.',
690
+ type: 'rate_limit_exceeded',
691
+ code: 'rate_limit_exceeded'
692
+ })
693
+ : config.errorEnvelope({
694
+ message: `Synthetic upstream error (${status}).`,
695
+ type: status >= 500 ? 'server_error' : 'api_error',
696
+ code: status >= 500 ? 'server_error' : 'upstream_error'
697
+ })
698
+
699
+ const kernel = makeEmulatorKernel({
700
+ routes: config.routes,
701
+ faultSchema: ResponsesFault,
702
+ turnSchema: ResponsesScriptedTurn,
703
+ newEntry: (base): MutableLedgerEntry => ({ ...base, toolNames: [], headers: {} }),
704
+ snapshotEntry: (entry): ResponsesLedgerEntry => ({
705
+ ...entry,
706
+ toolNames: [...entry.toolNames],
707
+ headers: { ...entry.headers }
708
+ }),
709
+ unknownRoute: () =>
710
+ errorResponse(404, {
711
+ message: 'Synthetic: no emulated route.',
712
+ type: 'invalid_request_error',
713
+ code: 'not_found'
714
+ }),
715
+ stateFields: () => ({ knownModels }),
716
+ inputInvalid: config.inputInvalid
717
+ })
718
+
719
+ const responses = async (
720
+ request: Request,
721
+ entry: MutableLedgerEntry,
722
+ path: string
723
+ ): Promise<Response> => {
724
+ for (const rule of config.headers) {
725
+ const value = request.headers.get(rule.name)
726
+
727
+ if (rule.record && value !== null) entry.headers[rule.name] = value
728
+ }
729
+
730
+ if (!bearerPattern.test(request.headers.get('authorization') ?? '')) {
731
+ entry.status = 401
732
+
733
+ return errorResponse(401, config.unauthorized)
734
+ }
735
+
736
+ for (const rule of config.headers) {
737
+ if (
738
+ rule.required !== undefined &&
739
+ (request.headers.get(rule.name) ?? '').trim().length === 0
740
+ ) {
741
+ entry.status = rule.required.status
742
+
743
+ return errorResponse(rule.required.status, rule.required.error)
744
+ }
745
+ }
746
+
747
+ const text = await readText(request)
748
+ const json = text === undefined ? undefined : parseJson(text)
749
+
750
+ if (text === undefined || json === undefined) {
751
+ entry.status = 400
752
+
753
+ return invalidRequest('Synthetic: the request body is not valid JSON.', 'invalid_json')
754
+ }
755
+
756
+ entry.body = json
757
+
758
+ const decoded = decodeResponsesRequest(json)
759
+
760
+ if (Result.isFailure(decoded) || !isJsonObject(json)) {
761
+ entry.status = 400
762
+
763
+ return invalidRequest(
764
+ 'Synthetic: the request body is not a Responses request (`input` is required).',
765
+ 'invalid_request'
766
+ )
767
+ }
768
+
769
+ const body = decoded.success
770
+ const seq = entry.seq
771
+
772
+ if (body.model !== undefined) entry.model = body.model
773
+
774
+ entry.stream = body.stream === true
775
+
776
+ if (Predicate.isBoolean(body.store)) entry.store = body.store
777
+
778
+ if (body.reasoning !== undefined) entry.reasoning = body.reasoning
779
+
780
+ if (body.tool_choice !== undefined) entry.toolChoice = body.tool_choice
781
+
782
+ entry.toolNames = functionTools(body).map(tool => tool.name)
783
+
784
+ const outputLimit = jsonField(json, 'max_output_tokens')
785
+
786
+ if (Predicate.isNumber(outputLimit)) entry.maxOutputTokens = outputLimit
787
+
788
+ if (config.outputTokenLimit === 'rejected' && outputLimit !== undefined) {
789
+ entry.status = 400
790
+
791
+ return invalidRequest(
792
+ 'Synthetic: unsupported parameter: max_output_tokens.',
793
+ 'unsupported_parameter',
794
+ 'max_output_tokens'
795
+ )
796
+ }
797
+
798
+ if (outputLimit !== undefined && outputLimit !== null && !isPositiveInteger(outputLimit)) {
799
+ entry.status = 400
800
+
801
+ return invalidRequest(
802
+ 'Synthetic: max_output_tokens must be a positive integer.',
803
+ 'invalid_value',
804
+ 'max_output_tokens'
805
+ )
806
+ }
807
+
808
+ const fault = kernel.takeFault(path, body.model)
809
+
810
+ if (fault !== undefined && fault.fault.kind === 'status') {
811
+ return kernel.respondWithStatusFault(entry, fault, fault.fault, defaultFaultBody)
812
+ }
813
+
814
+ const errorEventFault =
815
+ fault !== undefined && fault.fault.kind === 'error-event-after-chunks'
816
+ ? { state: fault, fault: fault.fault }
817
+ : undefined
818
+
819
+ const cannotApply = (problem: string): Response => {
820
+ entry.faultError = problem
821
+ entry.status = 500
822
+
823
+ return controlError(500, `emulator fault cannot apply: ${problem}`)
824
+ }
825
+
826
+ // A scripted turn is used up when its request arrives. Header and status validation at
827
+ // `script.enqueue` time means its response can always be built.
828
+ const turn = kernel.nextTurn()
829
+
830
+ if (turn !== undefined && 'error' in turn) {
831
+ entry.scripted = 'error'
832
+
833
+ if (errorEventFault !== undefined) {
834
+ return cannotApply('error-event-after-chunks cannot apply to a scripted error')
835
+ }
836
+
837
+ return kernel.respondWithScriptedError(entry, turn.error, fault)
838
+ }
839
+
840
+ if (turn === undefined && (body.model === undefined || !knownModels.includes(body.model))) {
841
+ entry.status = config.unknownModel.status
842
+
843
+ return errorResponse(config.unknownModel.status, config.unknownModel.error)
844
+ }
845
+
846
+ if (turn !== undefined) entry.scripted = 'response'
847
+
848
+ const plan =
849
+ turn === undefined ? defaultPlan(body, seq, config.defaultText) : scriptedPlan(turn, seq)
850
+
851
+ const identity = { id: `resp_synthetic_${seq}`, model: body.model ?? 'unknown' }
852
+ const usage = planUsage(plan, text)
853
+ const format = plan.format ?? (body.stream === true ? 'sse' : 'json')
854
+
855
+ if (format === 'json') {
856
+ if (errorEventFault !== undefined) {
857
+ return cannotApply('error-event-after-chunks cannot apply to a JSON response')
858
+ }
859
+
860
+ return kernel.respondWithBody(
861
+ entry,
862
+ 200,
863
+ { 'content-type': 'application/json' },
864
+ [JSON.stringify(completedResponse(plan, identity, usage))],
865
+ fault
866
+ )
867
+ }
868
+
869
+ const events = streamEvents(plan, identity, usage)
870
+ const headers = { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }
871
+
872
+ if (errorEventFault === undefined) {
873
+ return kernel.respondWithBody(entry, 200, headers, events, fault)
874
+ }
875
+
876
+ const after = errorEventFault.fault.chunks
877
+
878
+ // The error must arrive before `response.completed`, or a client rightly ignores it.
879
+ if (after >= events.length) {
880
+ return cannotApply(
881
+ `error-event-after-chunks after ${after} chunk(s) cannot apply to a response with ${events.length} chunk(s)`
882
+ )
883
+ }
884
+
885
+ const response = kernel.respondWithBody(
886
+ entry,
887
+ 200,
888
+ headers,
889
+ [...events.slice(0, after), errorEvent(errorEventFault.fault, identity, after)],
890
+ undefined
891
+ )
892
+
893
+ kernel.consumeFault(errorEventFault.state, entry)
894
+
895
+ return response
896
+ }
897
+
898
+ // Every manifest route maps to its own handler; construction throws `EmulatorRouteUnmapped`
899
+ // when a manifest route has no handler (or a handler has no manifest route).
900
+ return kernel.serve(new Map([[emulatorRouteKey('POST', config.path), responses]]))
901
+ }