@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.
- package/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- package/src/xai.ts +341 -0
package/src/messages.ts
ADDED
|
@@ -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
|
+
}
|