@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/todoist.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful Todoist emulator (API v1 under `https://api.todoist.com/api/v1`), built on the upstream
|
|
3
|
+
* `@emulators/core` custom runtime, with a request ledger, status faults, and an `/_emulate/*`
|
|
4
|
+
* control plane.
|
|
5
|
+
*
|
|
6
|
+
* Fixture-only: it never imports SDK code; its wire shapes and default seed are copied as data
|
|
7
|
+
* from the synthetic Todoist conformance fixtures, and every route names the conformance cases it
|
|
8
|
+
* follows in `todoistEmulatorRoutes`. Only the routes the seven Todoist cases (with their cleanup)
|
|
9
|
+
* need are emulated; anything the fixtures do not record answers one ledgered 400
|
|
10
|
+
* `{ error: { type: 'not_emulated', message } }`. That includes the project listing of the
|
|
11
|
+
* read-only leftover lookup (`findTodoistConformanceLeftovers`), which no fixture records: the
|
|
12
|
+
* lookup fails (`todoist_list_projects_failed`, HTTP 400), and runners print their lookup-failed
|
|
13
|
+
* WARN instead of leftover warnings.
|
|
14
|
+
*
|
|
15
|
+
* Fail closed: a request on no emulated route (or with a raw id segment that is not a Todoist id)
|
|
16
|
+
* is ledgered and answered with constant text only (`/<unrecognised>`, a standard method or
|
|
17
|
+
* `<other>`, no query or body, a constant reason). So is a request whose `Authorization` header is
|
|
18
|
+
* present but is not one recognisable bearer, whatever its route: its credential cannot be
|
|
19
|
+
* extracted and scrubbed.
|
|
20
|
+
*
|
|
21
|
+
* No answer is synthesised: a seeded project (whose object no fixture records) answers
|
|
22
|
+
* not-emulated on a read; only items created through the recorded create flow are updated,
|
|
23
|
+
* closed, or deleted; a case project takes one task and a seeded parent one sub-project (the
|
|
24
|
+
* fixtures record `child_order: 1` only). Minted ids use the reserved prefix `6XEmu`, which seeds
|
|
25
|
+
* may not use. The bearer value is guarded like the Telegram token: scrubbed from everything
|
|
26
|
+
* ledgered or answered, and a path, query, or body that repeats it is refused.
|
|
27
|
+
*
|
|
28
|
+
* Request-shape latitude (the only accepted deviations from the fixture requests): any credential
|
|
29
|
+
* value of at least 8 characters that occurs nowhere else in the request (its path, query, or body;
|
|
30
|
+
* never checked against anything, stored, or ledgered); extra request headers; `content-type`
|
|
31
|
+
* parameters; query parameters in any order; any Todoist id (1-64 of `[A-Za-z0-9_-]`) of an
|
|
32
|
+
* existing item where a fixture has an id (reads: seeded or created tasks and created projects;
|
|
33
|
+
* task listings: the paging project with `limit=2` and case projects created here without `limit`;
|
|
34
|
+
* writes: only items created through the recorded create flow; a new project's `parent_id`: a
|
|
35
|
+
* seeded project); any `run-` run id (at most 40 characters) in a case project name; any non-empty
|
|
36
|
+
* task `content`; a task update sending `content`, `due_datetime`, or both; a label listing `limit`
|
|
37
|
+
* of 1 to 200 that covers every label.
|
|
38
|
+
*
|
|
39
|
+
* Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
|
|
40
|
+
* `makeTodoistEmulator` (importing this module has no side effects).
|
|
41
|
+
*
|
|
42
|
+
* @experimental
|
|
43
|
+
*/
|
|
44
|
+
import { Data, Predicate } from 'effect'
|
|
45
|
+
import type { EmulatorRouteEvidence } from './route-evidence.ts'
|
|
46
|
+
import { statefulCoreRuntime } from './stateful-core.ts'
|
|
47
|
+
import {
|
|
48
|
+
makeHeaderlessStatefulEmulator,
|
|
49
|
+
routeEvidence,
|
|
50
|
+
type StatefulCoverage,
|
|
51
|
+
type StatefulFault,
|
|
52
|
+
type StatefulFaultState,
|
|
53
|
+
type StatefulHeaderlessLedgerEntry,
|
|
54
|
+
type StatefulErrorTexts,
|
|
55
|
+
type StatefulInputKind,
|
|
56
|
+
type StatefulResolution
|
|
57
|
+
} from './stateful-emulator.ts'
|
|
58
|
+
import {
|
|
59
|
+
matchTodoistRoute,
|
|
60
|
+
todoistApiRoutes,
|
|
61
|
+
todoistQueryProblem,
|
|
62
|
+
type TodoistApiEnv,
|
|
63
|
+
type TodoistCursor,
|
|
64
|
+
type TodoistEmulatorDrills
|
|
65
|
+
} from './todoist/api.ts'
|
|
66
|
+
import {
|
|
67
|
+
buildTodoistSeedState,
|
|
68
|
+
decodeTodoistState,
|
|
69
|
+
type TodoistEmulatorSeed,
|
|
70
|
+
type TodoistEmulatorState
|
|
71
|
+
} from './todoist/state.ts'
|
|
72
|
+
|
|
73
|
+
export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
|
|
74
|
+
|
|
75
|
+
export { emulatorEvidenceHeader } from './route-evidence.ts'
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
StatefulFault as TodoistFault,
|
|
79
|
+
StatefulFaultMatch as TodoistFaultMatch
|
|
80
|
+
} from './stateful-emulator.ts'
|
|
81
|
+
|
|
82
|
+
export { todoistEmulatorBasePath, type TodoistEmulatorDrills } from './todoist/api.ts'
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
TodoistEmulatorDue,
|
|
86
|
+
TodoistEmulatorLabel,
|
|
87
|
+
TodoistEmulatorProfile,
|
|
88
|
+
TodoistEmulatorProject,
|
|
89
|
+
TodoistEmulatorProjectSeed,
|
|
90
|
+
TodoistEmulatorSeed,
|
|
91
|
+
TodoistEmulatorStateSchema,
|
|
92
|
+
TodoistEmulatorTask,
|
|
93
|
+
TodoistEmulatorTaskSeed,
|
|
94
|
+
type TodoistEmulatorState
|
|
95
|
+
} from './todoist/state.ts'
|
|
96
|
+
|
|
97
|
+
/** Origin the connector calls. */
|
|
98
|
+
export const todoistEmulatorDefaultOrigin = 'https://api.todoist.com'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Route evidence manifest: every emulated route, whether it writes, and the conformance cases
|
|
102
|
+
* whose (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers
|
|
103
|
+
* by construction (both come from one route table). Every route cites at least one case.
|
|
104
|
+
*/
|
|
105
|
+
export const todoistEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = todoistApiRoutes.map(
|
|
106
|
+
({ route }) => routeEvidence(route)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
export type TodoistFaultState = StatefulFaultState
|
|
110
|
+
|
|
111
|
+
export type TodoistLedgerEntry = StatefulHeaderlessLedgerEntry
|
|
112
|
+
|
|
113
|
+
export type TodoistCoverage = StatefulCoverage
|
|
114
|
+
|
|
115
|
+
/** Invalid emulator input from the JS API: a seed or a fault. A programmer error. */
|
|
116
|
+
export class TodoistEmulatorInputInvalid extends Data.TaggedError('TodoistEmulatorInputInvalid')<{
|
|
117
|
+
readonly input: 'seed' | 'fault'
|
|
118
|
+
readonly reason: string
|
|
119
|
+
}> {
|
|
120
|
+
override get message(): string {
|
|
121
|
+
return `Invalid Todoist emulator ${this.input}: ${this.reason}`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type TodoistEmulatorOptions = {
|
|
126
|
+
/** Typed seed; defaults to the fixture entities (`profile: 'default'`). */
|
|
127
|
+
readonly seed?: TodoistEmulatorSeed
|
|
128
|
+
/** Clock in epoch milliseconds (created and closed timestamps). Defaults to `Date.now`. */
|
|
129
|
+
readonly now?: () => number
|
|
130
|
+
/** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
|
|
131
|
+
readonly drills?: TodoistEmulatorDrills
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type TodoistEmulator = {
|
|
135
|
+
/** The fetch handler (Todoist routes and `/_emulate/*`). Never rejects. */
|
|
136
|
+
readonly fetch: (request: Request) => Promise<Response>
|
|
137
|
+
readonly ledger: {
|
|
138
|
+
readonly entries: () => ReadonlyArray<TodoistLedgerEntry>
|
|
139
|
+
readonly clear: () => void
|
|
140
|
+
}
|
|
141
|
+
readonly faults: {
|
|
142
|
+
/** Add a fault; throws `TodoistEmulatorInputInvalid` for an invalid fault. */
|
|
143
|
+
readonly add: (fault: StatefulFault) => TodoistFaultState
|
|
144
|
+
readonly list: () => ReadonlyArray<TodoistFaultState>
|
|
145
|
+
readonly clear: () => void
|
|
146
|
+
}
|
|
147
|
+
/** Task-listing cursors answered since the last reset or seed (runtime data). */
|
|
148
|
+
readonly cursors: () => ReadonlyArray<TodoistCursor>
|
|
149
|
+
/** Restore the current seed and clear the ledger, faults, and cursors. */
|
|
150
|
+
readonly reset: () => Promise<void>
|
|
151
|
+
/**
|
|
152
|
+
* Replace the state with a new seed, which becomes what `reset` restores (cursors are cleared).
|
|
153
|
+
* Rejects with `TodoistEmulatorInputInvalid` for an invalid seed.
|
|
154
|
+
*/
|
|
155
|
+
readonly seed: (seed: TodoistEmulatorSeed) => Promise<void>
|
|
156
|
+
/** A deep copy of the current state (entities and counters). */
|
|
157
|
+
readonly snapshot: () => TodoistEmulatorState
|
|
158
|
+
readonly coverage: () => TodoistCoverage
|
|
159
|
+
/** Close the core runtime. Later requests answer 503. Idempotent. */
|
|
160
|
+
readonly close: () => Promise<void>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A bearer credential of at least 8 characters. The value is never checked against anything,
|
|
164
|
+
// stored, forwarded, or ledgered; it is guarded like the Telegram token: a path, query, or body
|
|
165
|
+
// that repeats it is refused, and it is scrubbed from everything ledgered or answered.
|
|
166
|
+
const bearerPattern = /^bearer\s+(\S{8,})\s*$/i
|
|
167
|
+
|
|
168
|
+
const resolve = (
|
|
169
|
+
request: Request,
|
|
170
|
+
url: URL
|
|
171
|
+
): StatefulResolution<TodoistEmulatorState, TodoistApiEnv> => {
|
|
172
|
+
const ledgerPath = url.pathname
|
|
173
|
+
const authorization = request.headers.get('authorization')
|
|
174
|
+
const bearer = bearerPattern.exec(authorization ?? '')?.[1]
|
|
175
|
+
|
|
176
|
+
// Fail closed: an Authorization header that is present but not one recognisable bearer (extra
|
|
177
|
+
// words, duplicated headers) means the credential cannot be extracted and scrubbed, so the
|
|
178
|
+
// request is ledgered and answered with constant text only, whatever the route.
|
|
179
|
+
if (authorization !== null && bearer === undefined) {
|
|
180
|
+
return {
|
|
181
|
+
kind: 'unrecognised',
|
|
182
|
+
reason: 'an unrecognisable Authorization header is not emulated'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// On a recognised route the bearer value is guarded (scrubbed and never repeated).
|
|
187
|
+
const secrets = bearer === undefined ? [] : [bearer]
|
|
188
|
+
const matched = matchTodoistRoute(request.method, url.pathname)
|
|
189
|
+
|
|
190
|
+
// Fail closed: a request on no emulated route is ledgered and answered with constant text only.
|
|
191
|
+
if (matched === undefined) {
|
|
192
|
+
return { kind: 'unrecognised', reason: 'no emulated Todoist route for this method and path' }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const refuse = (reason: string): StatefulResolution<TodoistEmulatorState, TodoistApiEnv> => ({
|
|
196
|
+
kind: 'refused',
|
|
197
|
+
ledgerPath,
|
|
198
|
+
reason,
|
|
199
|
+
route: matched.route.route,
|
|
200
|
+
secrets
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (bearer === undefined) {
|
|
204
|
+
return refuse(
|
|
205
|
+
'requests without Authorization: Bearer <token of at least 8 characters> are not emulated'
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const problem = todoistQueryProblem(matched.route, url.searchParams)
|
|
210
|
+
|
|
211
|
+
if (problem !== undefined) return refuse(problem)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
kind: 'route',
|
|
215
|
+
ledgerPath,
|
|
216
|
+
route: matched.route.route,
|
|
217
|
+
params: matched.params,
|
|
218
|
+
// The path and query the request must not repeat the bearer in.
|
|
219
|
+
guardedPath: `${url.pathname}${url.search}`,
|
|
220
|
+
secrets
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The texts of Todoist's recovery answers (500 failed, 503 closed, 500 unhandled). */
|
|
225
|
+
const errorTexts: StatefulErrorTexts = {
|
|
226
|
+
failed: 'Synthetic: the emulator could not build the response.',
|
|
227
|
+
closed: 'Synthetic: the emulator is closed.',
|
|
228
|
+
unhandled: 'emulator failed to handle the request'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The wrapper reports invalid seeds and faults only (Todoist takes no other option input). */
|
|
232
|
+
const inputInvalid = (input: StatefulInputKind, reason: string) =>
|
|
233
|
+
new TodoistEmulatorInputInvalid({ input: input === 'seed' ? 'seed' : 'fault', reason })
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a stateful Todoist emulator on the `@emulators/core` custom runtime. Each call has its
|
|
237
|
+
* own state, ledger, faults, and cursors. Rejects with `TodoistEmulatorInputInvalid` for an
|
|
238
|
+
* invalid seed.
|
|
239
|
+
*
|
|
240
|
+
* Routes (under `/api/v1`, `Authorization: Bearer <token of at least 8 characters>`): task
|
|
241
|
+
* listing with cursor paging, task read, create, update, and close, label listing, and project
|
|
242
|
+
* create, read, and delete.
|
|
243
|
+
* See `README.md` (Todoist emulator) for the wire claims.
|
|
244
|
+
*/
|
|
245
|
+
export const makeTodoistEmulator = async (
|
|
246
|
+
options: TodoistEmulatorOptions = {}
|
|
247
|
+
): Promise<TodoistEmulator> => {
|
|
248
|
+
const initial = buildTodoistSeedState(options.seed ?? {})
|
|
249
|
+
|
|
250
|
+
if (Predicate.isString(initial)) {
|
|
251
|
+
throw new TodoistEmulatorInputInvalid({ input: 'seed', reason: initial })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const drills = options.drills ?? {}
|
|
255
|
+
|
|
256
|
+
const env: TodoistApiEnv = {
|
|
257
|
+
now: options.now ?? (() => Date.now()),
|
|
258
|
+
drills: {
|
|
259
|
+
cursorRestarts: drills.cursorRestarts ?? false,
|
|
260
|
+
notFoundWithoutError: drills.notFoundWithoutError ?? false,
|
|
261
|
+
taskLabelsAsIds: drills.taskLabelsAsIds ?? false,
|
|
262
|
+
listIncludesClosed: drills.listIncludesClosed ?? false,
|
|
263
|
+
ignoreDue: drills.ignoreDue ?? false,
|
|
264
|
+
createOmitsParent: drills.createOmitsParent ?? false,
|
|
265
|
+
deleteKeepsTasks: drills.deleteKeepsTasks ?? false
|
|
266
|
+
},
|
|
267
|
+
cursors: new Map(),
|
|
268
|
+
cursorCounter: { next: 1 }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const cursorList = (): ReadonlyArray<TodoistCursor> => [...env.cursors.values()]
|
|
272
|
+
|
|
273
|
+
const emulator = await makeHeaderlessStatefulEmulator<
|
|
274
|
+
TodoistEmulatorState,
|
|
275
|
+
TodoistApiEnv,
|
|
276
|
+
TodoistEmulatorSeed
|
|
277
|
+
>(
|
|
278
|
+
{
|
|
279
|
+
routes: todoistApiRoutes.map(({ route }) => route),
|
|
280
|
+
env,
|
|
281
|
+
initial,
|
|
282
|
+
buildSeed: buildTodoistSeedState,
|
|
283
|
+
resolveRequest: resolve,
|
|
284
|
+
errorTexts,
|
|
285
|
+
clearRuntime: () => {
|
|
286
|
+
env.cursors.clear()
|
|
287
|
+
env.cursorCounter.next = 1
|
|
288
|
+
},
|
|
289
|
+
runtimeState: () => ({ runtime: { cursors: cursorList().map(cursor => ({ ...cursor })) } }),
|
|
290
|
+
seedSummary: state => ({
|
|
291
|
+
projects: state.projects.length,
|
|
292
|
+
tasks: state.tasks.length,
|
|
293
|
+
labels: state.labels.length
|
|
294
|
+
}),
|
|
295
|
+
inputInvalid
|
|
296
|
+
},
|
|
297
|
+
statefulCoreRuntime({
|
|
298
|
+
name: 'todoist',
|
|
299
|
+
initial,
|
|
300
|
+
decodeState: decodeTodoistState,
|
|
301
|
+
inputInvalid
|
|
302
|
+
})
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
fetch: emulator.fetch,
|
|
307
|
+
ledger: emulator.ledger,
|
|
308
|
+
faults: emulator.faults,
|
|
309
|
+
cursors: cursorList,
|
|
310
|
+
reset: emulator.reset,
|
|
311
|
+
seed: emulator.seed,
|
|
312
|
+
snapshot: emulator.snapshot,
|
|
313
|
+
coverage: emulator.coverage,
|
|
314
|
+
close: emulator.close
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/xai.ts
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xAI Grok CLI proxy Responses emulator: a plain fetch handler for `POST /v1/responses` (origin
|
|
3
|
+
* `https://cli-chat-proxy.grok.com`), with scripted turns, wire faults, a request ledger, and an
|
|
4
|
+
* `/_emulate/*` control plane.
|
|
5
|
+
*
|
|
6
|
+
* It never imports SDK code: its wire shapes follow the synthetic Grok Responses conformance
|
|
7
|
+
* fixtures (Responses SSE with typed `event:` names, the `response` JSON body, and the
|
|
8
|
+
* `{ error: { message, type, param, code } }` envelope) and are linked to those conformance case
|
|
9
|
+
* ids in `xAiGrokEmulatorRoutes`. The Responses machinery lives in the internal `responses.ts`
|
|
10
|
+
* core and is shared with the Codex emulator.
|
|
11
|
+
*
|
|
12
|
+
* Requests authenticate with a non-empty `Authorization: Bearer` credential (a Grok OAuth access
|
|
13
|
+
* token) and must also send, in this order, a non-empty `X-XAI-Token-Auth` (401 without it; never
|
|
14
|
+
* recorded or checked), `x-grok-client-version` (426 without it: the proxy version-gates
|
|
15
|
+
* requests), and `x-grok-model-override` (400 without it). The client version and model override
|
|
16
|
+
* are recorded in the ledger; the version value itself is not checked.
|
|
17
|
+
*
|
|
18
|
+
* The same fetch handler also answers the Grok subscription-usage route
|
|
19
|
+
* (`GET /v1/billing?format=credits`, `emulator.usage`) with its own manifest
|
|
20
|
+
* (`xAiGrokSubscriptionUsageEmulatorRoutes`), ledger, faults, and turns; see
|
|
21
|
+
* `makeXAiGrokEmulator`.
|
|
22
|
+
*
|
|
23
|
+
* Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`, `TextEncoder`, `URL`);
|
|
24
|
+
* no Effect runtime is required to use it.
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
28
|
+
import { Data } from 'effect'
|
|
29
|
+
import type * as Schema from 'effect/Schema'
|
|
30
|
+
import { withSubscriptionUsage } from './emulator-compose.ts'
|
|
31
|
+
import type {
|
|
32
|
+
EmulatorCoverage,
|
|
33
|
+
EmulatorFaultState,
|
|
34
|
+
EmulatorRouteCoverage
|
|
35
|
+
} from './emulator-kernel.ts'
|
|
36
|
+
import { EmulatorFaultMatch, EmulatorScriptedError } from './emulator-kernel.ts'
|
|
37
|
+
import {
|
|
38
|
+
makeResponsesEmulator,
|
|
39
|
+
ResponsesErrorEventFault,
|
|
40
|
+
ResponsesFault,
|
|
41
|
+
ResponsesScriptedFunctionCall,
|
|
42
|
+
ResponsesScriptedResponse,
|
|
43
|
+
ResponsesScriptedTurn,
|
|
44
|
+
ResponsesScriptedUsage,
|
|
45
|
+
ResponsesStreamError,
|
|
46
|
+
type ResponsesEmulator,
|
|
47
|
+
type ResponsesFaultKind,
|
|
48
|
+
type ResponsesLedgerEntry,
|
|
49
|
+
type ResponsesWireError
|
|
50
|
+
} from './responses.ts'
|
|
51
|
+
import type { EmulatorRouteEvidence } from './route-evidence.ts'
|
|
52
|
+
import {
|
|
53
|
+
makeSubscriptionUsageEmulator,
|
|
54
|
+
recordedUsageBody,
|
|
55
|
+
SubscriptionUsageFault,
|
|
56
|
+
type SubscriptionUsageEmulator,
|
|
57
|
+
type SubscriptionUsageLedgerEntry,
|
|
58
|
+
type SubscriptionUsageScriptedTurn
|
|
59
|
+
} from './subscription-usage.ts'
|
|
60
|
+
import { xAiGrokUsageRecording } from './subscription-usage-recordings.ts'
|
|
61
|
+
|
|
62
|
+
export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
|
|
63
|
+
|
|
64
|
+
export { emulatorEvidenceHeader } from './route-evidence.ts'
|
|
65
|
+
|
|
66
|
+
export const xAiGrokResponsesPath = '/v1/responses'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The Grok subscription-usage path; the SDK fetcher (`xAiGrokSubscriptionUsageUrl`) adds
|
|
70
|
+
* `?format=credits`, the recorded query.
|
|
71
|
+
*/
|
|
72
|
+
export const xAiGrokSubscriptionUsagePath = '/v1/billing'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Route evidence manifest of the Grok subscription-usage route (served by the same fetch handler
|
|
76
|
+
* as the Responses route, with its own ledger and coverage). Synthetic, unverified.
|
|
77
|
+
*/
|
|
78
|
+
export const xAiGrokSubscriptionUsageEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [
|
|
79
|
+
{
|
|
80
|
+
method: 'GET',
|
|
81
|
+
path: xAiGrokSubscriptionUsagePath,
|
|
82
|
+
kind: 'provider',
|
|
83
|
+
write: false,
|
|
84
|
+
caseIds: ['xai.grok.usage.snapshot'],
|
|
85
|
+
evidence: 'unverified',
|
|
86
|
+
observedAt: undefined
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The recorded Grok usage body (the synthetic `xai.grok.usage.snapshot` fixture, copied as data):
|
|
92
|
+
* `config.creditUsagePercent` and `config.currentPeriod` `{ type, start, end }`.
|
|
93
|
+
*/
|
|
94
|
+
export const xAiGrokSubscriptionUsageDefault: Schema.Json = recordedUsageBody(xAiGrokUsageRecording)
|
|
95
|
+
|
|
96
|
+
/** Synthetic-safe default model ids, including the Grok conformance defaults. */
|
|
97
|
+
export const xAiGrokEmulatorDefaultModels: ReadonlyArray<string> = ['grok-build', 'grok-4.6']
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Route evidence manifest: every emulated Grok route and the conformance cases whose (currently
|
|
101
|
+
* synthetic, unverified) wire shapes it follows.
|
|
102
|
+
*/
|
|
103
|
+
export const xAiGrokEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [
|
|
104
|
+
{
|
|
105
|
+
method: 'POST',
|
|
106
|
+
path: xAiGrokResponsesPath,
|
|
107
|
+
kind: 'provider',
|
|
108
|
+
write: false,
|
|
109
|
+
caseIds: [
|
|
110
|
+
'xai.grok.stream.plain-text',
|
|
111
|
+
'xai.grok.stream.function-call-arguments',
|
|
112
|
+
'xai.grok.stream.error-envelope',
|
|
113
|
+
'xai.grok.stream.terminal-event'
|
|
114
|
+
],
|
|
115
|
+
evidence: 'unverified',
|
|
116
|
+
observedAt: undefined
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
/** Optional fault filter; an omitted field matches every request. `path` ending in `*` is a prefix. */
|
|
121
|
+
export const XAiGrokFaultMatch = EmulatorFaultMatch
|
|
122
|
+
|
|
123
|
+
export type XAiGrokFaultMatch = typeof EmulatorFaultMatch.Type
|
|
124
|
+
|
|
125
|
+
/** A mid-stream Responses error (`{ code, message }`). */
|
|
126
|
+
export const XAiGrokStreamError = ResponsesStreamError
|
|
127
|
+
|
|
128
|
+
export type XAiGrokStreamError = ResponsesStreamError
|
|
129
|
+
|
|
130
|
+
/** A mid-stream `error` or `response.failed` event fault, streamed responses only. */
|
|
131
|
+
export const XAiGrokErrorEventFault = ResponsesErrorEventFault
|
|
132
|
+
|
|
133
|
+
export type XAiGrokErrorEventFault = typeof ResponsesErrorEventFault.Type
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wire faults: `status` (for example 429 with `retry-after`; the body defaults to the error
|
|
137
|
+
* envelope for the status), `error-after-chunks` (a dropped connection),
|
|
138
|
+
* `truncate-after-chunks` (a clean close, for example before `response.completed`), and
|
|
139
|
+
* `error-event-after-chunks` (a mid-stream `error` or `response.failed` event). Statuses that
|
|
140
|
+
* cannot carry a body and redirects are rejected, as are invalid headers and `location`. A fault
|
|
141
|
+
* that cannot take effect answers 500 and is not consumed.
|
|
142
|
+
*/
|
|
143
|
+
export const XAiGrokFault = ResponsesFault
|
|
144
|
+
|
|
145
|
+
export type XAiGrokFault = ResponsesFault
|
|
146
|
+
|
|
147
|
+
export type XAiGrokFaultKind = ResponsesFaultKind
|
|
148
|
+
|
|
149
|
+
/** Usage for a scripted turn (sent in `response.completed` or the JSON body). */
|
|
150
|
+
export const XAiGrokScriptedUsage = ResponsesScriptedUsage
|
|
151
|
+
|
|
152
|
+
export type XAiGrokScriptedUsage = ResponsesScriptedUsage
|
|
153
|
+
|
|
154
|
+
/** One scripted `function_call` item; its arguments stream as these fragments. */
|
|
155
|
+
export const XAiGrokScriptedFunctionCall = ResponsesScriptedFunctionCall
|
|
156
|
+
|
|
157
|
+
export type XAiGrokScriptedFunctionCall = ResponsesScriptedFunctionCall
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A scripted response: `reasoning` (summary fragments), `text`, `functionCalls`, `order`,
|
|
161
|
+
* `usage` (`null` drops it), and `format` (`sse` or `json`, default: the request's `stream`).
|
|
162
|
+
*/
|
|
163
|
+
export const XAiGrokScriptedResponse = ResponsesScriptedResponse
|
|
164
|
+
|
|
165
|
+
export type XAiGrokScriptedResponse = ResponsesScriptedResponse
|
|
166
|
+
|
|
167
|
+
/** A scripted error response: status, body (a string is sent as is), and optional headers. */
|
|
168
|
+
export const XAiGrokScriptedError = EmulatorScriptedError
|
|
169
|
+
|
|
170
|
+
export type XAiGrokScriptedError = typeof EmulatorScriptedError.Type
|
|
171
|
+
|
|
172
|
+
/** A turn queued for the next Grok request. */
|
|
173
|
+
export const XAiGrokScriptedTurn = ResponsesScriptedTurn
|
|
174
|
+
|
|
175
|
+
export type XAiGrokScriptedTurn = ResponsesScriptedTurn
|
|
176
|
+
|
|
177
|
+
/** Thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input; a programmer error. */
|
|
178
|
+
export class XAiGrokEmulatorInputInvalid extends Data.TaggedError('XAiGrokEmulatorInputInvalid')<{
|
|
179
|
+
readonly input: 'fault' | 'turn'
|
|
180
|
+
readonly reason: string
|
|
181
|
+
}> {
|
|
182
|
+
override get message(): string {
|
|
183
|
+
return `Invalid xAI Grok emulator ${this.input}: ${this.reason}`
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type XAiGrokLedgerEntry = ResponsesLedgerEntry
|
|
188
|
+
|
|
189
|
+
export type XAiGrokFaultState = EmulatorFaultState<XAiGrokFault>
|
|
190
|
+
|
|
191
|
+
export type XAiGrokRouteCoverage = EmulatorRouteCoverage
|
|
192
|
+
|
|
193
|
+
export type XAiGrokCoverage = EmulatorCoverage
|
|
194
|
+
|
|
195
|
+
/** A usage-route fault (`status`, `error-after-chunks`, `truncate-after-chunks`). */
|
|
196
|
+
export const XAiGrokUsageFault = SubscriptionUsageFault
|
|
197
|
+
|
|
198
|
+
export type XAiGrokUsageFault = SubscriptionUsageFault
|
|
199
|
+
|
|
200
|
+
/** A usage turn: `{ usage }` (a body with the recorded JSON shape) or `{ error }`. */
|
|
201
|
+
export type XAiGrokUsageScriptedTurn = SubscriptionUsageScriptedTurn
|
|
202
|
+
|
|
203
|
+
export type XAiGrokUsageLedgerEntry = SubscriptionUsageLedgerEntry
|
|
204
|
+
|
|
205
|
+
export type XAiGrokUsageEmulator = SubscriptionUsageEmulator
|
|
206
|
+
|
|
207
|
+
export type XAiGrokEmulatorOptions = {
|
|
208
|
+
/** Model ids that exist. Defaults to `xAiGrokEmulatorDefaultModels`. */
|
|
209
|
+
readonly knownModels?: ReadonlyArray<string>
|
|
210
|
+
/**
|
|
211
|
+
* Replacement usage-route body; must have the recorded JSON shape (same keys and value kinds).
|
|
212
|
+
* Defaults to the recorded body (`xAiGrokSubscriptionUsageDefault`).
|
|
213
|
+
*/
|
|
214
|
+
readonly subscriptionUsage?: Schema.Json
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The Responses emulator, plus `usage`: the subscription-usage route's own emulator API. */
|
|
218
|
+
export type XAiGrokEmulator = ResponsesEmulator & { readonly usage: XAiGrokUsageEmulator }
|
|
219
|
+
|
|
220
|
+
const errorEnvelope = (error: ResponsesWireError): Schema.Json => ({
|
|
221
|
+
error: {
|
|
222
|
+
message: error.message,
|
|
223
|
+
type: error.type,
|
|
224
|
+
param: error.param ?? null,
|
|
225
|
+
code: error.code
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create a Grok CLI proxy Responses emulator. Each call has independent ledger, fault, and script
|
|
231
|
+
* state.
|
|
232
|
+
*
|
|
233
|
+
* Without a script, `POST /v1/responses` answers a known model with synthetic output: `stream:
|
|
234
|
+
* true` streams `response.created`, `response.in_progress`, the output items
|
|
235
|
+
* (`response.output_item.added`, their parts and deltas, `response.output_item.done`), and
|
|
236
|
+
* `response.completed` with usage; `stream: false` returns one `response` JSON body. A request
|
|
237
|
+
* whose `reasoning` asks for a `summary` gets a reasoning item first. A request with function
|
|
238
|
+
* `tools` gets one `function_call` item whose arguments are synthesized from the tool's JSON
|
|
239
|
+
* Schema and streamed as `response.function_call_arguments.delta` fragments. Unknown models get
|
|
240
|
+
* 400 `model_not_found`; a request without a bearer credential gets 401 `invalid_api_key`,
|
|
241
|
+
* without `X-XAI-Token-Auth` 401, without `x-grok-client-version` 426, and without
|
|
242
|
+
* `x-grok-model-override` 400; a non-positive `max_output_tokens` gets 400; unknown routes get a
|
|
243
|
+
* 404 envelope. Not enforced: that `x-grok-model-override` matches the body `model`, the client
|
|
244
|
+
* version value, and the output limit itself (recorded as `maxOutputTokens`).
|
|
245
|
+
*
|
|
246
|
+
* `GET /v1/billing?format=credits` is fixture-only: a request with the headers the SDK fetcher
|
|
247
|
+
* sends (a non-empty bearer, `X-XAI-Token-Auth: xai-grok-cli`, a non-empty `x-userid`, a
|
|
248
|
+
* non-empty `x-grok-client-version`, and `x-grok-client-mode: headless`; the bearer, user id, and
|
|
249
|
+
* client-version values are request-shape latitude, and the user id and token-auth are never
|
|
250
|
+
* recorded),
|
|
251
|
+
* `accept: application/json`, and exactly the recorded `format=credits` query gets the recorded
|
|
252
|
+
* body (`xAiGrokSubscriptionUsageDefault`, or a same-shaped `options.subscriptionUsage` / scripted
|
|
253
|
+
* `{ usage }`); anything else answers 400 not-emulated. The client version and mode are recorded.
|
|
254
|
+
* Its ledger, faults, turns, and coverage are `emulator.usage`
|
|
255
|
+
* (control plane `/_emulate/usage/*`); `reset()` and `POST /_emulate/reset` reset both routes.
|
|
256
|
+
*/
|
|
257
|
+
export const makeXAiGrokEmulator = (options: XAiGrokEmulatorOptions = {}): XAiGrokEmulator =>
|
|
258
|
+
withSubscriptionUsage(
|
|
259
|
+
makeXAiGrokResponsesEmulator(options),
|
|
260
|
+
makeXAiGrokUsageEmulator(options),
|
|
261
|
+
xAiGrokSubscriptionUsagePath
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const makeXAiGrokUsageEmulator = (options: XAiGrokEmulatorOptions): XAiGrokUsageEmulator =>
|
|
265
|
+
makeSubscriptionUsageEmulator({
|
|
266
|
+
path: xAiGrokSubscriptionUsagePath,
|
|
267
|
+
routes: xAiGrokSubscriptionUsageEmulatorRoutes,
|
|
268
|
+
recording: xAiGrokUsageRecording,
|
|
269
|
+
headers: [
|
|
270
|
+
{ name: 'x-xai-token-auth', record: false, accepts: value => value === 'xai-grok-cli' },
|
|
271
|
+
{ name: 'x-userid', record: false },
|
|
272
|
+
{ name: 'x-grok-client-version', record: true },
|
|
273
|
+
{ name: 'x-grok-client-mode', record: true, accepts: value => value === 'headless' }
|
|
274
|
+
],
|
|
275
|
+
subscriptionUsage: options.subscriptionUsage,
|
|
276
|
+
inputInvalid: (input, reason) => new XAiGrokEmulatorInputInvalid({ input, reason })
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const makeXAiGrokResponsesEmulator = (options: XAiGrokEmulatorOptions): ResponsesEmulator =>
|
|
280
|
+
makeResponsesEmulator({
|
|
281
|
+
path: xAiGrokResponsesPath,
|
|
282
|
+
routes: xAiGrokEmulatorRoutes,
|
|
283
|
+
knownModels: options.knownModels ?? xAiGrokEmulatorDefaultModels,
|
|
284
|
+
errorEnvelope,
|
|
285
|
+
// Copied data shape of the synthetic Grok error-envelope fixture (unknown model id).
|
|
286
|
+
unknownModel: {
|
|
287
|
+
status: 400,
|
|
288
|
+
error: {
|
|
289
|
+
message: 'Synthetic placeholder: the requested model does not exist.',
|
|
290
|
+
type: 'invalid_request_error',
|
|
291
|
+
code: 'model_not_found',
|
|
292
|
+
param: 'model'
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
unauthorized: {
|
|
296
|
+
message: 'Synthetic: missing or invalid bearer credential.',
|
|
297
|
+
type: 'invalid_request_error',
|
|
298
|
+
code: 'invalid_api_key'
|
|
299
|
+
},
|
|
300
|
+
headers: [
|
|
301
|
+
{
|
|
302
|
+
name: 'x-xai-token-auth',
|
|
303
|
+
record: false,
|
|
304
|
+
required: {
|
|
305
|
+
status: 401,
|
|
306
|
+
error: {
|
|
307
|
+
message: 'Synthetic: the X-XAI-Token-Auth header is required.',
|
|
308
|
+
type: 'invalid_request_error',
|
|
309
|
+
code: 'missing_token_auth'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'x-grok-client-version',
|
|
315
|
+
record: true,
|
|
316
|
+
required: {
|
|
317
|
+
status: 426,
|
|
318
|
+
error: {
|
|
319
|
+
message: 'Synthetic: a supported x-grok-client-version header is required.',
|
|
320
|
+
type: 'invalid_request_error',
|
|
321
|
+
code: 'upgrade_required'
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'x-grok-model-override',
|
|
327
|
+
record: true,
|
|
328
|
+
required: {
|
|
329
|
+
status: 400,
|
|
330
|
+
error: {
|
|
331
|
+
message: 'Synthetic: the x-grok-model-override header is required.',
|
|
332
|
+
type: 'invalid_request_error',
|
|
333
|
+
code: 'missing_model_override'
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
outputTokenLimit: 'optional',
|
|
339
|
+
defaultText: ['Hello', ' from the', ' synthetic Grok emulator.'],
|
|
340
|
+
inputInvalid: (input, reason) => new XAiGrokEmulatorInputInvalid({ input, reason })
|
|
341
|
+
})
|