@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,844 @@
1
+ /**
2
+ * Anthropic Messages emulator core (internal; not a package export).
3
+ *
4
+ * One `POST /v1/messages` route with request parsing (`model`, `max_tokens`, `system`,
5
+ * `messages`, `tools`, `tool_choice`, `thinking`, `stream`), Messages SSE framing in the order
6
+ * the real API sends it (`message_start`, then per content block `content_block_start`, its
7
+ * deltas, and `content_block_stop`, a `ping` after the first block starts, then `message_delta`
8
+ * with the stop reason and usage, and `message_stop`), the non-streamed `message` JSON body,
9
+ * scripted turns, and the `{ type: 'error', error: { type, message } }` envelope. Faults, the
10
+ * request ledger, the `/_emulate/*` control plane, evidence tagging, and route binding come from
11
+ * the shared kernel (`emulator-kernel.ts`).
12
+ *
13
+ * Content blocks: `thinking` (`thinking_delta` fragments, then one `signature_delta`) when the
14
+ * request enables thinking, `text` (`text_delta`), and `tool_use` (`input_json_delta` fragments
15
+ * of the tool input). Stop reasons: `end_turn`, `tool_use`, and `max_tokens` (the default answer
16
+ * is cut when it would exceed the request's `max_tokens`; scripted turns name any reason).
17
+ *
18
+ * Requests authenticate with a non-empty `x-api-key` (native API) or `Authorization: Bearer`
19
+ * (OAuth) credential; neither value is ever checked or stored. They must send a supported
20
+ * `anthropic-version` (`2023-06-01`, the value the SDK providers send); a missing or other value
21
+ * answers 400 `invalid_request_error`. `thinking` together with a forced `tool_choice` (`tool` or
22
+ * `any`) answers 400 as well. Other API constraints (the OAuth `anthropic-beta` header for bearer
23
+ * credentials, `budget_tokens` limits) are not enforced.
24
+ *
25
+ * Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`, `TextEncoder`, `URL`);
26
+ * no Node builtins and no SDK imports.
27
+ */
28
+ import { Predicate, Result } from 'effect'
29
+ import * as Schema from 'effect/Schema'
30
+ import {
31
+ approximateTokens,
32
+ ChunkCount,
33
+ controlError,
34
+ EmulatorErrorAfterChunksFault,
35
+ emulatorFaultFields,
36
+ EmulatorScriptedError,
37
+ EmulatorStatusFault,
38
+ EmulatorTruncateAfterChunksFault,
39
+ isJsonObject,
40
+ jsonField,
41
+ jsonResponse,
42
+ makeEmulatorKernel,
43
+ parseJson,
44
+ readText,
45
+ splitFragments,
46
+ stringField,
47
+ synthesizeValue,
48
+ type EmulatorApi,
49
+ type KernelLedgerEntry
50
+ } from './emulator-kernel.ts'
51
+ import {
52
+ emulatorRouteKey,
53
+ type EmulatorEvidence,
54
+ type EmulatorRouteEvidence
55
+ } from './route-evidence.ts'
56
+
57
+ /** An Anthropic wire error: `type` (for example `overloaded_error`) and a message. */
58
+ export const MessagesWireError = Schema.Struct({
59
+ type: Schema.NonEmptyString,
60
+ message: Schema.String
61
+ })
62
+
63
+ export type MessagesWireError = typeof MessagesWireError.Type
64
+
65
+ /**
66
+ * Send `chunks` SSE events, then one `event: error` event (default `overloaded_error`) and close
67
+ * without `message_stop`. Streamed responses only; it must come before `message_stop`.
68
+ */
69
+ export const MessagesErrorEventFault = Schema.Struct({
70
+ kind: Schema.Literal('error-event-after-chunks'),
71
+ chunks: ChunkCount,
72
+ error: Schema.optionalKey(MessagesWireError),
73
+ ...emulatorFaultFields
74
+ })
75
+
76
+ /**
77
+ * Wire faults: `status` (the body defaults to an Anthropic error envelope matching the status,
78
+ * for example `rate_limit_error` for 429 and `overloaded_error` for 529), `error-after-chunks`
79
+ * (a dropped connection), `truncate-after-chunks` (a clean close, for example before
80
+ * `message_stop`), and `error-event-after-chunks` (a mid-stream `error` event).
81
+ */
82
+ export const MessagesFault = Schema.Union([
83
+ EmulatorStatusFault,
84
+ EmulatorErrorAfterChunksFault,
85
+ EmulatorTruncateAfterChunksFault,
86
+ MessagesErrorEventFault
87
+ ])
88
+
89
+ export type MessagesFault = typeof MessagesFault.Type
90
+
91
+ export type MessagesFaultKind = MessagesFault['kind']
92
+
93
+ const TokenCount = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
94
+
95
+ /** Usage for a scripted turn (sent in `message_start` and `message_delta`). */
96
+ export const MessagesScriptedUsage = Schema.Struct({
97
+ inputTokens: TokenCount,
98
+ outputTokens: TokenCount
99
+ })
100
+
101
+ export type MessagesScriptedUsage = typeof MessagesScriptedUsage.Type
102
+
103
+ /** One scripted `tool_use` block; its JSON input streams as these `input_json_delta` fragments. */
104
+ export const MessagesScriptedToolUse = Schema.Struct({
105
+ name: Schema.NonEmptyString,
106
+ inputFragments: Schema.Array(Schema.String),
107
+ id: Schema.optionalKey(Schema.NonEmptyString)
108
+ })
109
+
110
+ export type MessagesScriptedToolUse = typeof MessagesScriptedToolUse.Type
111
+
112
+ /**
113
+ * A scripted message. Every field is exact (nothing is filled in) except: a block is sent only
114
+ * when its field is present; `usage` omitted is synthesized and `null` drops `usage` from the
115
+ * wire; `stopReason` omitted is `tool_use` with tool uses, else `end_turn`; `order` defaults to
116
+ * `thinking-first` (`text-first` sends the thinking block after the text).
117
+ */
118
+ export const MessagesScriptedMessage = Schema.Struct({
119
+ thinking: Schema.optionalKey(Schema.Array(Schema.String)),
120
+ text: Schema.optionalKey(Schema.Array(Schema.String)),
121
+ toolUses: Schema.optionalKey(Schema.Array(MessagesScriptedToolUse)),
122
+ order: Schema.optionalKey(Schema.Literals(['thinking-first', 'text-first'])),
123
+ usage: Schema.optionalKey(Schema.NullOr(MessagesScriptedUsage)),
124
+ stopReason: Schema.optionalKey(Schema.NonEmptyString)
125
+ })
126
+
127
+ export type MessagesScriptedMessage = typeof MessagesScriptedMessage.Type
128
+
129
+ /** A turn queued for the next Messages request. */
130
+ export const MessagesScriptedTurn = Schema.Union([EmulatorScriptedError, MessagesScriptedMessage])
131
+
132
+ export type MessagesScriptedTurn = typeof MessagesScriptedTurn.Type
133
+
134
+ export type MessagesLedgerEntry = {
135
+ /** 1-based arrival order since the last ledger clear or reset. */
136
+ readonly seq: number
137
+ readonly method: string
138
+ readonly path: string
139
+ /** Parsed JSON request body, when it was valid JSON. */
140
+ readonly body?: Schema.Json
141
+ readonly model?: string
142
+ readonly stream?: boolean
143
+ /** The request's `max_tokens`, when it is a number (non-positive or fractional answers 400). */
144
+ readonly maxTokens?: number
145
+ readonly thinking?: Schema.Json
146
+ readonly toolChoice?: Schema.Json
147
+ readonly toolNames: ReadonlyArray<string>
148
+ /** Which header carried the credential (the value is never recorded). */
149
+ readonly credentialHeader?: 'x-api-key' | 'authorization'
150
+ readonly anthropicVersion?: string
151
+ readonly anthropicBeta?: string
152
+ /** Kind of the fault that shaped the response. */
153
+ readonly fault?: MessagesFaultKind
154
+ /** Why a matching fault could not take effect (the response was a 500 emulator error). */
155
+ readonly faultError?: string
156
+ /**
157
+ * Set when the emulator could not build the planned response; the request was answered with a
158
+ * 500 emulator error (still evidence-tagged) and no fault was used up.
159
+ */
160
+ readonly responseError?: string
161
+ /** Set when a scripted turn answered the request. */
162
+ readonly scripted?: 'message' | 'error'
163
+ /** Evidence of the matched route; `unknown-route` for requests that failed closed. */
164
+ readonly evidence: EmulatorEvidence | 'unknown-route'
165
+ readonly status: number
166
+ /** Body chunks handed to the transport so far (each produced when pulled). */
167
+ readonly bodyChunks: number
168
+ }
169
+
170
+ export type MessagesEmulator = EmulatorApi<MessagesScriptedTurn, MessagesFault, MessagesLedgerEntry>
171
+
172
+ export type MessagesEmulatorConfig = {
173
+ /** The Messages path, for example `/v1/messages`. */
174
+ readonly path: string
175
+ /** Route evidence manifest; must list exactly the Messages route. */
176
+ readonly routes: ReadonlyArray<EmulatorRouteEvidence>
177
+ readonly knownModels: ReadonlyArray<string>
178
+ /** Builds the error thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input. */
179
+ readonly inputInvalid: (input: 'fault' | 'turn', reason: string) => Error
180
+ }
181
+
182
+ const MessagesTool = Schema.Struct({
183
+ name: Schema.String,
184
+ type: Schema.optionalKey(Schema.String),
185
+ description: Schema.optionalKey(Schema.String),
186
+ input_schema: Schema.optionalKey(Schema.Json)
187
+ })
188
+
189
+ const MessagesRequest = Schema.Struct({
190
+ model: Schema.optionalKey(Schema.String),
191
+ max_tokens: Schema.optionalKey(Schema.Json),
192
+ system: Schema.optionalKey(Schema.Json),
193
+ messages: Schema.Array(Schema.Json),
194
+ tools: Schema.optionalKey(Schema.NullOr(Schema.Array(MessagesTool))),
195
+ tool_choice: Schema.optionalKey(Schema.Json),
196
+ thinking: Schema.optionalKey(Schema.Json),
197
+ stream: Schema.optionalKey(Schema.NullOr(Schema.Boolean))
198
+ })
199
+
200
+ type MessagesRequest = typeof MessagesRequest.Type
201
+
202
+ const decodeMessagesRequest = Schema.decodeUnknownResult(MessagesRequest)
203
+
204
+ // A non-empty bearer credential. The value is never checked or stored.
205
+ const bearerPattern = /^bearer\s+\S+/i
206
+
207
+ /** `anthropic-version` values the emulator accepts; the SDK providers send `2023-06-01` by default. */
208
+ const supportedAnthropicVersions: ReadonlyArray<string> = ['2023-06-01']
209
+
210
+ const defaultText = ['Hello', ' from the', ' synthetic Anthropic emulator.']
211
+
212
+ const defaultThinking = ['The user wants a short greeting.', ' Reply briefly.']
213
+
214
+ // Opaque placeholder for the thinking-block signature the real API sends.
215
+ const syntheticSignature = 'synthetic-thinking-signature'
216
+
217
+ type BlockPlan =
218
+ | { readonly type: 'thinking'; readonly fragments: ReadonlyArray<string> }
219
+ | { readonly type: 'text'; readonly fragments: ReadonlyArray<string> }
220
+ | {
221
+ readonly type: 'tool_use'
222
+ readonly id: string
223
+ readonly name: string
224
+ readonly fragments: ReadonlyArray<string>
225
+ }
226
+
227
+ type MessagePlan = {
228
+ readonly blocks: ReadonlyArray<BlockPlan>
229
+ /** `null`: never send usage; `undefined`: synthesize. */
230
+ readonly usage: MessagesScriptedUsage | null | undefined
231
+ readonly stopReason: string
232
+ }
233
+
234
+ const thinkingRequested = (request: MessagesRequest): boolean => {
235
+ const type = stringField(request.thinking, 'type')
236
+
237
+ return type === 'enabled' || type === 'adaptive'
238
+ }
239
+
240
+ // The real API rejects extended thinking when `tool_choice` forces tool use.
241
+ const forcedToolUse = (request: MessagesRequest): boolean => {
242
+ const choice = stringField(request.tool_choice, 'type')
243
+
244
+ return choice === 'tool' || choice === 'any'
245
+ }
246
+
247
+ const chosenTool = (request: MessagesRequest) => {
248
+ const tools = request.tools ?? []
249
+ const choice = stringField(request.tool_choice, 'type')
250
+
251
+ if (tools.length === 0 || choice === 'none') {
252
+ return undefined
253
+ }
254
+
255
+ const forced = choice === 'tool' ? stringField(request.tool_choice, 'name') : undefined
256
+
257
+ return tools.find(tool => tool.name === forced) ?? tools[0]
258
+ }
259
+
260
+ /**
261
+ * Cut the plan so its synthetic output fits `maxTokens` (about four characters per token):
262
+ * thinking and text are cut mid-fragment, a tool call that does not fit is dropped, and the stop
263
+ * reason becomes `max_tokens`.
264
+ */
265
+ const fitToMaxTokens = (plan: MessagePlan, maxTokens: number): MessagePlan => {
266
+ let budget = maxTokens * 4
267
+ const blocks: Array<BlockPlan> = []
268
+
269
+ for (const block of plan.blocks) {
270
+ const size = block.fragments.join('').length
271
+
272
+ if (size <= budget) {
273
+ blocks.push(block)
274
+ budget -= size
275
+
276
+ continue
277
+ }
278
+
279
+ if (block.type !== 'tool_use' && budget > 0) {
280
+ const fragments: Array<string> = []
281
+
282
+ for (const fragment of block.fragments) {
283
+ if (budget <= 0) break
284
+
285
+ fragments.push(fragment.slice(0, budget))
286
+ budget -= Math.min(fragment.length, budget)
287
+ }
288
+
289
+ blocks.push({ type: block.type, fragments })
290
+ }
291
+
292
+ return { blocks, usage: plan.usage, stopReason: 'max_tokens' }
293
+ }
294
+
295
+ return plan
296
+ }
297
+
298
+ const defaultPlan = (request: MessagesRequest, seq: number, maxTokens: number): MessagePlan => {
299
+ const thinking: ReadonlyArray<BlockPlan> = thinkingRequested(request)
300
+ ? [{ type: 'thinking', fragments: defaultThinking }]
301
+ : []
302
+
303
+ const tool = chosenTool(request)
304
+
305
+ if (tool === undefined) {
306
+ return fitToMaxTokens(
307
+ {
308
+ blocks: [...thinking, { type: 'text', fragments: defaultText }],
309
+ usage: undefined,
310
+ stopReason: 'end_turn'
311
+ },
312
+ maxTokens
313
+ )
314
+ }
315
+
316
+ const input = JSON.stringify(
317
+ synthesizeValue(tool.input_schema ?? { type: 'object' }, tool.name, 0)
318
+ )
319
+
320
+ return fitToMaxTokens(
321
+ {
322
+ blocks: [
323
+ ...thinking,
324
+ {
325
+ type: 'tool_use',
326
+ id: `toolu_synthetic_${seq}_0`,
327
+ name: tool.name,
328
+ // Emulator choice (unverified): open the input with an empty `partial_json` fragment.
329
+ fragments: ['', ...splitFragments(input, 3)]
330
+ }
331
+ ],
332
+ usage: undefined,
333
+ stopReason: 'tool_use'
334
+ },
335
+ maxTokens
336
+ )
337
+ }
338
+
339
+ const scriptedPlan = (turn: MessagesScriptedMessage, seq: number): MessagePlan => {
340
+ const thinking: ReadonlyArray<BlockPlan> =
341
+ turn.thinking === undefined ? [] : [{ type: 'thinking', fragments: turn.thinking }]
342
+
343
+ const text: ReadonlyArray<BlockPlan> =
344
+ turn.text === undefined ? [] : [{ type: 'text', fragments: turn.text }]
345
+
346
+ const toolUses = (turn.toolUses ?? []).map((call, index): BlockPlan => ({
347
+ type: 'tool_use',
348
+ id: call.id ?? `toolu_synthetic_${seq}_${index}`,
349
+ name: call.name,
350
+ fragments: call.inputFragments
351
+ }))
352
+
353
+ return {
354
+ blocks:
355
+ turn.order === 'text-first'
356
+ ? [...text, ...thinking, ...toolUses]
357
+ : [...thinking, ...text, ...toolUses],
358
+ usage: turn.usage,
359
+ stopReason: turn.stopReason ?? (toolUses.length > 0 ? 'tool_use' : 'end_turn')
360
+ }
361
+ }
362
+
363
+ type PlanUsage = { readonly inputTokens: number; readonly outputTokens: number } | undefined
364
+
365
+ const planUsage = (plan: MessagePlan, requestText: string): PlanUsage => {
366
+ if (plan.usage === null) {
367
+ return undefined
368
+ }
369
+
370
+ return (
371
+ plan.usage ?? {
372
+ inputTokens: approximateTokens(requestText),
373
+ outputTokens: approximateTokens(plan.blocks.flatMap(block => block.fragments).join(''))
374
+ }
375
+ )
376
+ }
377
+
378
+ type ResponseIdentity = { readonly id: string; readonly model: string }
379
+
380
+ // Wire shapes of the Messages responses the emulator sends.
381
+ type WireUsage = {
382
+ readonly input_tokens: number
383
+ readonly cache_creation_input_tokens: number
384
+ readonly cache_read_input_tokens: number
385
+ readonly output_tokens: number
386
+ }
387
+
388
+ type WireMessage = {
389
+ readonly id: string
390
+ readonly type: 'message'
391
+ readonly role: 'assistant'
392
+ readonly model: string
393
+ readonly content: ReadonlyArray<Schema.JsonObject>
394
+ readonly stop_reason: string | null
395
+ readonly stop_sequence: null
396
+ usage?: WireUsage
397
+ }
398
+
399
+ type WireMessageDelta = {
400
+ readonly delta: { readonly stop_reason: string; readonly stop_sequence: null }
401
+ usage?: WireUsage
402
+ }
403
+
404
+ const wireUsage = (inputTokens: number, outputTokens: number): WireUsage => ({
405
+ input_tokens: inputTokens,
406
+ cache_creation_input_tokens: 0,
407
+ cache_read_input_tokens: 0,
408
+ output_tokens: outputTokens
409
+ })
410
+
411
+ const sseEvent = (type: string, payload: Schema.JsonObject): string =>
412
+ `event: ${type}\ndata: ${JSON.stringify({ type, ...payload })}\n\n`
413
+
414
+ const blockStart = (block: BlockPlan): Schema.JsonObject => {
415
+ switch (block.type) {
416
+ case 'thinking':
417
+ return { type: 'thinking', thinking: '', signature: '' }
418
+ case 'text':
419
+ return { type: 'text', text: '' }
420
+ case 'tool_use':
421
+ return { type: 'tool_use', id: block.id, name: block.name, input: {} }
422
+ }
423
+ }
424
+
425
+ const blockDelta = (block: BlockPlan, fragment: string): Schema.JsonObject => {
426
+ switch (block.type) {
427
+ case 'thinking':
428
+ return { type: 'thinking_delta', thinking: fragment }
429
+ case 'text':
430
+ return { type: 'text_delta', text: fragment }
431
+ case 'tool_use':
432
+ return { type: 'input_json_delta', partial_json: fragment }
433
+ }
434
+ }
435
+
436
+ /** SSE events for a streamed message, one network chunk each, in the real API's order. */
437
+ const streamEvents = (
438
+ plan: MessagePlan,
439
+ identity: ResponseIdentity,
440
+ usage: PlanUsage
441
+ ): ReadonlyArray<string> => {
442
+ const message: WireMessage = {
443
+ id: identity.id,
444
+ type: 'message',
445
+ role: 'assistant',
446
+ model: identity.model,
447
+ content: [],
448
+ stop_reason: null,
449
+ stop_sequence: null
450
+ }
451
+
452
+ if (usage !== undefined) {
453
+ message.usage = wireUsage(usage.inputTokens, 1)
454
+ }
455
+
456
+ const events: Array<string> = [sseEvent('message_start', { message })]
457
+ const ping = sseEvent('ping', {})
458
+
459
+ plan.blocks.forEach((block, index) => {
460
+ events.push(sseEvent('content_block_start', { index, content_block: blockStart(block) }))
461
+
462
+ if (index === 0) {
463
+ events.push(ping)
464
+ }
465
+
466
+ for (const fragment of block.fragments) {
467
+ events.push(sseEvent('content_block_delta', { index, delta: blockDelta(block, fragment) }))
468
+ }
469
+
470
+ if (block.type === 'thinking') {
471
+ events.push(
472
+ sseEvent('content_block_delta', {
473
+ index,
474
+ delta: { type: 'signature_delta', signature: syntheticSignature }
475
+ })
476
+ )
477
+ }
478
+
479
+ events.push(sseEvent('content_block_stop', { index }))
480
+ })
481
+
482
+ if (plan.blocks.length === 0) {
483
+ events.push(ping)
484
+ }
485
+
486
+ const messageDelta: WireMessageDelta = {
487
+ delta: { stop_reason: plan.stopReason, stop_sequence: null }
488
+ }
489
+
490
+ // Same usage shape as the committed fixtures: input and cache counts alongside the cumulative
491
+ // `output_tokens`. Unverified until a live recording confirms it.
492
+ if (usage !== undefined) {
493
+ messageDelta.usage = wireUsage(usage.inputTokens, usage.outputTokens)
494
+ }
495
+
496
+ events.push(sseEvent('message_delta', messageDelta))
497
+ events.push(sseEvent('message_stop', {}))
498
+
499
+ return events
500
+ }
501
+
502
+ /** The JSON value of a tool input; throws (answering an emulator 500) when it is not JSON. */
503
+ const toolInput = (fragments: ReadonlyArray<string>): Schema.Json => {
504
+ const text = fragments.join('')
505
+ const parsed = text.trim().length === 0 ? {} : parseJson(text)
506
+
507
+ if (parsed === undefined) {
508
+ throw new Error('scripted tool input is not JSON')
509
+ }
510
+
511
+ return parsed
512
+ }
513
+
514
+ /** The non-streamed `message` JSON body. */
515
+ const messageBody = (
516
+ plan: MessagePlan,
517
+ identity: ResponseIdentity,
518
+ usage: PlanUsage
519
+ ): WireMessage => {
520
+ const content = plan.blocks.map((block): Schema.JsonObject => {
521
+ switch (block.type) {
522
+ case 'thinking':
523
+ return {
524
+ type: 'thinking',
525
+ thinking: block.fragments.join(''),
526
+ signature: syntheticSignature
527
+ }
528
+ case 'text':
529
+ return { type: 'text', text: block.fragments.join('') }
530
+ case 'tool_use':
531
+ return {
532
+ type: 'tool_use',
533
+ id: block.id,
534
+ name: block.name,
535
+ input: toolInput(block.fragments)
536
+ }
537
+ }
538
+ })
539
+
540
+ const body: WireMessage = {
541
+ id: identity.id,
542
+ type: 'message',
543
+ role: 'assistant',
544
+ model: identity.model,
545
+ content,
546
+ stop_reason: plan.stopReason,
547
+ stop_sequence: null
548
+ }
549
+
550
+ if (usage !== undefined) {
551
+ body.usage = wireUsage(usage.inputTokens, usage.outputTokens)
552
+ }
553
+
554
+ return body
555
+ }
556
+
557
+ /** The Anthropic error envelope. */
558
+ export const messagesErrorEnvelope = (error: MessagesWireError): Schema.JsonObject => ({
559
+ type: 'error',
560
+ error: { type: error.type, message: error.message }
561
+ })
562
+
563
+ const errorTypeForStatus = (status: number): string => {
564
+ switch (status) {
565
+ case 400:
566
+ return 'invalid_request_error'
567
+ case 401:
568
+ return 'authentication_error'
569
+ case 402:
570
+ return 'billing_error'
571
+ case 403:
572
+ return 'permission_error'
573
+ case 404:
574
+ return 'not_found_error'
575
+ case 413:
576
+ return 'request_too_large'
577
+ case 429:
578
+ return 'rate_limit_error'
579
+ case 529:
580
+ return 'overloaded_error'
581
+ default:
582
+ return 'api_error'
583
+ }
584
+ }
585
+
586
+ const defaultFaultMessage = (status: number): string => {
587
+ switch (status) {
588
+ case 429:
589
+ return 'Synthetic rate limit: too many requests.'
590
+ case 529:
591
+ return 'Synthetic: overloaded.'
592
+ default:
593
+ return `Synthetic upstream error (${status}).`
594
+ }
595
+ }
596
+
597
+ const defaultFaultBody = (status: number): Schema.Json =>
598
+ messagesErrorEnvelope({ type: errorTypeForStatus(status), message: defaultFaultMessage(status) })
599
+
600
+ const defaultErrorEvent: MessagesWireError = {
601
+ type: 'overloaded_error',
602
+ message: 'Synthetic: overloaded.'
603
+ }
604
+
605
+ type MutableLedgerEntry = KernelLedgerEntry<MessagesFaultKind> & {
606
+ body?: Schema.Json
607
+ model?: string
608
+ stream?: boolean
609
+ maxTokens?: number
610
+ thinking?: Schema.Json
611
+ toolChoice?: Schema.Json
612
+ toolNames: ReadonlyArray<string>
613
+ credentialHeader?: 'x-api-key' | 'authorization'
614
+ anthropicVersion?: string
615
+ anthropicBeta?: string
616
+ scripted?: 'message' | 'error'
617
+ }
618
+
619
+ const credentialHeaderOf = (request: Request): 'x-api-key' | 'authorization' | undefined => {
620
+ if ((request.headers.get('x-api-key') ?? '').trim().length > 0) {
621
+ return 'x-api-key'
622
+ }
623
+
624
+ return bearerPattern.test(request.headers.get('authorization') ?? '')
625
+ ? 'authorization'
626
+ : undefined
627
+ }
628
+
629
+ const isPositiveInteger = (value: Schema.Json | undefined): value is number =>
630
+ Predicate.isNumber(value) && Number.isSafeInteger(value) && value > 0
631
+
632
+ /**
633
+ * Create a Messages emulator from its config. Each call has independent ledger, fault, and
634
+ * script state.
635
+ *
636
+ * Precedence per request: authentication, a supported `anthropic-version`, JSON and request
637
+ * validation (including a positive integer `max_tokens`, and no `thinking` with a forced
638
+ * `tool_choice`), then the first matching fault if it is a `status` fault, then the next
639
+ * scripted turn, then model validation and defaults; a first matching body fault then shapes the
640
+ * body. Only the first matching fault (in insertion order) applies. Credential headers are never
641
+ * recorded, and credential values are never checked or stored.
642
+ */
643
+ export const makeMessagesEmulator = (config: MessagesEmulatorConfig): MessagesEmulator => {
644
+ const knownModels = [...config.knownModels]
645
+
646
+ const errorResponse = (status: number, error: MessagesWireError): Response =>
647
+ jsonResponse(status, messagesErrorEnvelope(error))
648
+
649
+ const invalidRequest = (message: string): Response =>
650
+ errorResponse(400, { type: 'invalid_request_error', message })
651
+
652
+ const kernel = makeEmulatorKernel({
653
+ routes: config.routes,
654
+ faultSchema: MessagesFault,
655
+ turnSchema: MessagesScriptedTurn,
656
+ newEntry: (base): MutableLedgerEntry => ({ ...base, toolNames: [] }),
657
+ snapshotEntry: (entry): MessagesLedgerEntry => ({ ...entry, toolNames: [...entry.toolNames] }),
658
+ unknownRoute: () =>
659
+ errorResponse(404, { type: 'not_found_error', message: 'Synthetic: no emulated route.' }),
660
+ stateFields: () => ({ knownModels }),
661
+ inputInvalid: config.inputInvalid
662
+ })
663
+
664
+ const messages = async (
665
+ request: Request,
666
+ entry: MutableLedgerEntry,
667
+ path: string
668
+ ): Promise<Response> => {
669
+ const credentialHeader = credentialHeaderOf(request)
670
+
671
+ if (credentialHeader === undefined) {
672
+ entry.status = 401
673
+
674
+ return errorResponse(401, {
675
+ type: 'authentication_error',
676
+ message: 'Synthetic: an x-api-key header or a bearer credential is required.'
677
+ })
678
+ }
679
+
680
+ entry.credentialHeader = credentialHeader
681
+
682
+ const version = request.headers.get('anthropic-version')
683
+ const beta = request.headers.get('anthropic-beta')
684
+
685
+ if (version !== null) entry.anthropicVersion = version
686
+
687
+ if (beta !== null) entry.anthropicBeta = beta
688
+
689
+ if (version === null || !supportedAnthropicVersions.includes(version)) {
690
+ entry.status = 400
691
+
692
+ return invalidRequest(
693
+ version === null
694
+ ? 'Synthetic: the anthropic-version header is required.'
695
+ : 'Synthetic: the anthropic-version header value is not supported.'
696
+ )
697
+ }
698
+
699
+ const text = await readText(request)
700
+ const json = text === undefined ? undefined : parseJson(text)
701
+
702
+ if (text === undefined || json === undefined) {
703
+ entry.status = 400
704
+
705
+ return invalidRequest('Synthetic: the request body is not valid JSON.')
706
+ }
707
+
708
+ entry.body = json
709
+
710
+ const decoded = decodeMessagesRequest(json)
711
+
712
+ if (Result.isFailure(decoded) || !isJsonObject(json)) {
713
+ entry.status = 400
714
+
715
+ return invalidRequest('Synthetic: the request body is not a Messages request.')
716
+ }
717
+
718
+ const body = decoded.success
719
+ const seq = entry.seq
720
+
721
+ if (body.model !== undefined) entry.model = body.model
722
+
723
+ entry.stream = body.stream === true
724
+ entry.toolNames = (body.tools ?? []).map(tool => tool.name)
725
+
726
+ if (Predicate.isNumber(body.max_tokens)) entry.maxTokens = body.max_tokens
727
+
728
+ if (body.thinking !== undefined) entry.thinking = body.thinking
729
+
730
+ if (body.tool_choice !== undefined) entry.toolChoice = body.tool_choice
731
+
732
+ const maxTokens = jsonField(json, 'max_tokens')
733
+
734
+ if (!isPositiveInteger(maxTokens)) {
735
+ entry.status = 400
736
+
737
+ return invalidRequest('Synthetic: max_tokens must be a positive integer.')
738
+ }
739
+
740
+ if (thinkingRequested(body) && forcedToolUse(body)) {
741
+ entry.status = 400
742
+
743
+ return invalidRequest(
744
+ 'Synthetic: thinking may not be enabled when tool_choice forces tool use.'
745
+ )
746
+ }
747
+
748
+ const fault = kernel.takeFault(path, body.model)
749
+
750
+ if (fault !== undefined && fault.fault.kind === 'status') {
751
+ return kernel.respondWithStatusFault(entry, fault, fault.fault, defaultFaultBody)
752
+ }
753
+
754
+ const errorEventFault =
755
+ fault !== undefined && fault.fault.kind === 'error-event-after-chunks'
756
+ ? { state: fault, fault: fault.fault }
757
+ : undefined
758
+
759
+ const cannotApply = (problem: string): Response => {
760
+ entry.faultError = problem
761
+ entry.status = 500
762
+
763
+ return controlError(500, `emulator fault cannot apply: ${problem}`)
764
+ }
765
+
766
+ // A scripted turn is used up when its request arrives. Header and status validation at
767
+ // `script.enqueue` time means its response can always be built.
768
+ const turn = kernel.nextTurn()
769
+
770
+ if (turn !== undefined && 'error' in turn) {
771
+ entry.scripted = 'error'
772
+
773
+ if (errorEventFault !== undefined) {
774
+ return cannotApply('error-event-after-chunks cannot apply to a scripted error')
775
+ }
776
+
777
+ return kernel.respondWithScriptedError(entry, turn.error, fault)
778
+ }
779
+
780
+ if (turn === undefined && (body.model === undefined || !knownModels.includes(body.model))) {
781
+ entry.status = 404
782
+
783
+ return errorResponse(404, {
784
+ type: 'not_found_error',
785
+ message: 'Synthetic placeholder: the requested model was not found.'
786
+ })
787
+ }
788
+
789
+ if (turn !== undefined) entry.scripted = 'message'
790
+
791
+ const plan = turn === undefined ? defaultPlan(body, seq, maxTokens) : scriptedPlan(turn, seq)
792
+ const identity = { id: `msg_synthetic_${seq}`, model: body.model ?? 'unknown' }
793
+ const usage = planUsage(plan, text)
794
+
795
+ if (body.stream !== true) {
796
+ if (errorEventFault !== undefined) {
797
+ return cannotApply('error-event-after-chunks cannot apply to a non-streamed response')
798
+ }
799
+
800
+ return kernel.respondWithBody(
801
+ entry,
802
+ 200,
803
+ { 'content-type': 'application/json' },
804
+ [JSON.stringify(messageBody(plan, identity, usage))],
805
+ fault
806
+ )
807
+ }
808
+
809
+ const events = streamEvents(plan, identity, usage)
810
+ const headers = { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }
811
+
812
+ if (errorEventFault === undefined) {
813
+ return kernel.respondWithBody(entry, 200, headers, events, fault)
814
+ }
815
+
816
+ const after = errorEventFault.fault.chunks
817
+
818
+ // The error must arrive before `message_stop`, or a client rightly ignores it.
819
+ if (after >= events.length) {
820
+ return cannotApply(
821
+ `error-event-after-chunks after ${after} chunk(s) cannot apply to a response with ${events.length} chunk(s)`
822
+ )
823
+ }
824
+
825
+ const response = kernel.respondWithBody(
826
+ entry,
827
+ 200,
828
+ headers,
829
+ [
830
+ ...events.slice(0, after),
831
+ sseEvent('error', { error: { ...(errorEventFault.fault.error ?? defaultErrorEvent) } })
832
+ ],
833
+ undefined
834
+ )
835
+
836
+ kernel.consumeFault(errorEventFault.state, entry)
837
+
838
+ return response
839
+ }
840
+
841
+ // Every manifest route maps to its own handler; construction throws `EmulatorRouteUnmapped`
842
+ // when a manifest route has no handler (or a handler has no manifest route).
843
+ return kernel.serve(new Map([[emulatorRouteKey('POST', config.path), messages]]))
844
+ }