@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Gateway classifier (`POST /v1/evaluate`) recordings the `/gateway` evaluate route answers
|
|
3
|
+
* from.
|
|
4
|
+
*
|
|
5
|
+
* Copied as data from the committed (synthetic, unverified) classifier conformance fixtures; never
|
|
6
|
+
* imported. `test/fixture-recordings.test.ts` fails when a fixture changes and this copy does not.
|
|
7
|
+
* Internal; not a package export.
|
|
8
|
+
*/
|
|
9
|
+
import type { FixtureRecording } from './fixture-route.ts'
|
|
10
|
+
|
|
11
|
+
const approvalRequestBody = (model: string) => ({
|
|
12
|
+
model,
|
|
13
|
+
state: 'Synthetic reply: thanks, the fix works on my side.',
|
|
14
|
+
questions: {
|
|
15
|
+
approves: {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
instructions: 'Does the reply approve the delivered result?',
|
|
18
|
+
criteria: {
|
|
19
|
+
true: 'The reply accepts or approves the result.',
|
|
20
|
+
false: 'The reply rejects or questions the result.'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const jsonHeaders = {
|
|
27
|
+
accept: 'application/json',
|
|
28
|
+
'content-type': 'application/json'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const jsonResponseHeaders = {
|
|
32
|
+
'content-type': 'application/json'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const gatewayEvaluateBooleanRecording: FixtureRecording = {
|
|
36
|
+
fixtureId: 'vercel-ai-gateway.classify.boolean.synthetic',
|
|
37
|
+
caseId: 'vercel-ai-gateway.classify.boolean',
|
|
38
|
+
request: {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
path: '/v1/evaluate',
|
|
41
|
+
query: '',
|
|
42
|
+
headers: jsonHeaders,
|
|
43
|
+
body: approvalRequestBody('typesafe-ai/jev')
|
|
44
|
+
},
|
|
45
|
+
response: {
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: jsonResponseHeaders,
|
|
48
|
+
streamed: false,
|
|
49
|
+
chunks: [
|
|
50
|
+
'{"model":"typesafe-ai/jev","answers":{"approves":{"type":"boolean","probability":0.93}},"usage":{"inputTokens":58,"outputTokens":0},"providerMetadata":{"gateway":{"cost":"0.000002436"}}}'
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const gatewayEvaluateChoiceRecording: FixtureRecording = {
|
|
56
|
+
fixtureId: 'vercel-ai-gateway.classify.choice.synthetic',
|
|
57
|
+
caseId: 'vercel-ai-gateway.classify.choice',
|
|
58
|
+
request: {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
path: '/v1/evaluate',
|
|
61
|
+
query: '',
|
|
62
|
+
headers: jsonHeaders,
|
|
63
|
+
body: {
|
|
64
|
+
model: 'typesafe-ai/jev',
|
|
65
|
+
state: {
|
|
66
|
+
subject: 'Synthetic invoice question',
|
|
67
|
+
body: 'I was charged twice for the synthetic plan.'
|
|
68
|
+
},
|
|
69
|
+
questions: {
|
|
70
|
+
route: {
|
|
71
|
+
type: 'choice',
|
|
72
|
+
instructions: 'Which team should handle this ticket?',
|
|
73
|
+
criteria: {
|
|
74
|
+
billing: 'Payments, invoices, and refunds.',
|
|
75
|
+
bug: 'Product defects and errors.',
|
|
76
|
+
other: 'Anything else.'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
response: {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: jsonResponseHeaders,
|
|
85
|
+
streamed: false,
|
|
86
|
+
chunks: [
|
|
87
|
+
'{"model":"typesafe-ai/jev","answers":{"route":{"type":"choice","choice":"billing","probabilities":{"billing":0.91,"bug":0.06,"other":0.03}}},"usage":{"inputTokens":74,"outputTokens":0},"providerMetadata":{"gateway":{"cost":"0.000003108"},"typesafe":{"confidence":{"route":0.88}}}}'
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const gatewayEvaluateScoreRecording: FixtureRecording = {
|
|
93
|
+
fixtureId: 'vercel-ai-gateway.classify.score.synthetic',
|
|
94
|
+
caseId: 'vercel-ai-gateway.classify.score',
|
|
95
|
+
request: {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
path: '/v1/evaluate',
|
|
98
|
+
query: '',
|
|
99
|
+
headers: jsonHeaders,
|
|
100
|
+
body: {
|
|
101
|
+
model: 'typesafe-ai/jev',
|
|
102
|
+
state: [
|
|
103
|
+
{
|
|
104
|
+
author: 'synthetic-user',
|
|
105
|
+
text: 'The synthetic dashboard is down for everyone.'
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
questions: {
|
|
109
|
+
urgency: {
|
|
110
|
+
type: 'score',
|
|
111
|
+
instructions: 'How urgent is this report?',
|
|
112
|
+
criteria: ['Not urgent.', 'Somewhat urgent.', 'Urgent.', 'Critical outage.']
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
response: {
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: jsonResponseHeaders,
|
|
120
|
+
streamed: false,
|
|
121
|
+
chunks: [
|
|
122
|
+
'{"model":"typesafe-ai/jev","answers":{"urgency":{"type":"score","score":2.74,"probabilities":{"0":0.01,"1":0.04,"2":0.15,"3":0.8},"confidence":0.77}},"usage":{"inputTokens":66,"outputTokens":0},"providerMetadata":{"gateway":{"cost":"0.000002772"}}}'
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const gatewayEvaluateErrorEnvelopeRecording: FixtureRecording = {
|
|
128
|
+
fixtureId: 'vercel-ai-gateway.classify.error-envelope.synthetic',
|
|
129
|
+
caseId: 'vercel-ai-gateway.classify.error-envelope',
|
|
130
|
+
request: {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
path: '/v1/evaluate',
|
|
133
|
+
query: '',
|
|
134
|
+
headers: jsonHeaders,
|
|
135
|
+
body: approvalRequestBody('yolk-conformance/model-does-not-exist')
|
|
136
|
+
},
|
|
137
|
+
response: {
|
|
138
|
+
status: 404,
|
|
139
|
+
headers: jsonResponseHeaders,
|
|
140
|
+
streamed: false,
|
|
141
|
+
chunks: [
|
|
142
|
+
'{"message":"Model \'yolk-conformance/model-does-not-exist\' not found","error_type":"model_not_found"}'
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Every classifier recording, in conformance case order. */
|
|
148
|
+
export const gatewayEvaluateRecordings: ReadonlyArray<FixtureRecording> = [
|
|
149
|
+
gatewayEvaluateBooleanRecording,
|
|
150
|
+
gatewayEvaluateChoiceRecording,
|
|
151
|
+
gatewayEvaluateScoreRecording,
|
|
152
|
+
gatewayEvaluateErrorEnvelopeRecording
|
|
153
|
+
]
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel AI Gateway emulator: a plain fetch handler for the OpenAI-compatible
|
|
3
|
+
* `POST /v1/chat/completions` endpoint, with scripted turns, wire faults, a
|
|
4
|
+
* request ledger, and an `/_emulate/*` control plane.
|
|
5
|
+
*
|
|
6
|
+
* It never imports SDK code: its wire shapes follow the verified live Gateway
|
|
7
|
+
* conformance recordings (2026-09-30), copied as data: `chat.completion.chunk`
|
|
8
|
+
* SSE with several events per network chunk, `delta.reasoning` plus
|
|
9
|
+
* `reasoning_details` for DeepSeek reasoning, a finish event carrying
|
|
10
|
+
* `provider_metadata`, `usage`, `system_fingerprint`, `service_tier`, and
|
|
11
|
+
* `generationId`, then `data: [DONE]`, and the 404 `model_not_found`
|
|
12
|
+
* envelope `{ error: { message, type, param: { modelId } } }` for unknown
|
|
13
|
+
* models. Ids, costs, and routing metadata are synthetic stand-ins. The route
|
|
14
|
+
* is linked to those conformance case ids in `gatewayEmulatorRoutes`
|
|
15
|
+
* (`verified`). The Chat Completions machinery is shared with the OpenAI
|
|
16
|
+
* emulator (`chat-completions.ts`); this module supplies the Gateway's paths,
|
|
17
|
+
* models, envelope, reasoning, and wire profile.
|
|
18
|
+
*
|
|
19
|
+
* Not covered by a recording (synthetic): the 401 error, default fault
|
|
20
|
+
* bodies, the non-streamed `chat.completion` body, and the answer to a request
|
|
21
|
+
* without a `model` (404 `Model '' not found` with `param.modelId: null`).
|
|
22
|
+
*
|
|
23
|
+
* The same fetch handler also answers the classifier route `POST /v1/evaluate`
|
|
24
|
+
* (AI Gateway calls classification "evaluation"; `emulator.evaluate`) with its
|
|
25
|
+
* own manifest (`gatewayEvaluateEmulatorRoutes`, unverified), ledger, faults,
|
|
26
|
+
* and turns. It is fixture-only: a request matching one of the four synthetic
|
|
27
|
+
* classifier conformance recordings (boolean, choice, score, unknown-model
|
|
28
|
+
* error envelope; `gateway-evaluate-recordings.ts`, copied as data) within the
|
|
29
|
+
* shared request-shape latitude gets that recording's response; anything else
|
|
30
|
+
* answers 400 not-emulated.
|
|
31
|
+
*
|
|
32
|
+
* Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`,
|
|
33
|
+
* `TextEncoder`, `URL`); no Effect runtime is required to use it.
|
|
34
|
+
*
|
|
35
|
+
* @experimental
|
|
36
|
+
*/
|
|
37
|
+
import { Data } from 'effect'
|
|
38
|
+
import type * as Schema from 'effect/Schema'
|
|
39
|
+
import {
|
|
40
|
+
ChatFault,
|
|
41
|
+
ChatFaultMatch,
|
|
42
|
+
ChatScriptedReasoningCompletion,
|
|
43
|
+
ChatScriptedError,
|
|
44
|
+
ChatScriptedToolCall,
|
|
45
|
+
ChatScriptedReasoningTurn,
|
|
46
|
+
ChatScriptedUsage,
|
|
47
|
+
makeChatCompletionsEmulator,
|
|
48
|
+
type ChatCompletionsEmulator,
|
|
49
|
+
type ChatCoverage,
|
|
50
|
+
type ChatFaultKind,
|
|
51
|
+
type ChatFaultState,
|
|
52
|
+
type ChatLedgerEntry,
|
|
53
|
+
type ChatResponseIdentity,
|
|
54
|
+
type ChatRouteCoverage,
|
|
55
|
+
type ChatUsageCounts,
|
|
56
|
+
type ChatWireError,
|
|
57
|
+
type ChatWireProfile
|
|
58
|
+
} from './chat-completions.ts'
|
|
59
|
+
import { composeFetch } from './emulator-compose.ts'
|
|
60
|
+
import {
|
|
61
|
+
FixtureRouteFault,
|
|
62
|
+
makeFixtureRouteEmulator,
|
|
63
|
+
type FixtureRouteEmulator,
|
|
64
|
+
type FixtureRouteFaultKind,
|
|
65
|
+
type FixtureRouteLedgerEntry,
|
|
66
|
+
type FixtureRouteScriptedTurn
|
|
67
|
+
} from './fixture-route.ts'
|
|
68
|
+
import { gatewayEvaluateRecordings } from './gateway-evaluate-recordings.ts'
|
|
69
|
+
import type { EmulatorRouteEvidence } from './route-evidence.ts'
|
|
70
|
+
|
|
71
|
+
export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
|
|
72
|
+
|
|
73
|
+
export { emulatorEvidenceHeader } from './route-evidence.ts'
|
|
74
|
+
|
|
75
|
+
export const gatewayChatCompletionsPath = '/v1/chat/completions'
|
|
76
|
+
|
|
77
|
+
/** The AI Gateway classifier ("evaluation") path. */
|
|
78
|
+
export const gatewayEvaluatePath = '/v1/evaluate'
|
|
79
|
+
|
|
80
|
+
/** Synthetic-safe default model ids, including the Gateway conformance defaults. */
|
|
81
|
+
export const gatewayEmulatorDefaultModels: ReadonlyArray<string> = [
|
|
82
|
+
'openai/gpt-4.1-nano',
|
|
83
|
+
'openai/gpt-4.1-mini',
|
|
84
|
+
'deepseek/deepseek-v3.2',
|
|
85
|
+
'deepseek/deepseek-v4.1-flash'
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/** Models that stream reasoning deltas when reasoning is requested. */
|
|
89
|
+
export const gatewayEmulatorDefaultReasoningModels: ReadonlyArray<string> = [
|
|
90
|
+
'deepseek/deepseek-v3.2',
|
|
91
|
+
'deepseek/deepseek-v4.1-flash'
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SSE events per network chunk by default. The live Gateway packs several
|
|
96
|
+
* events into one network chunk (the recordings carry one to four, always
|
|
97
|
+
* with the finish event and `data: [DONE]` together in the last chunk).
|
|
98
|
+
*/
|
|
99
|
+
export const gatewayEmulatorDefaultEventsPerChunk = 2
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Route evidence manifest: every emulated Gateway route and the conformance
|
|
103
|
+
* cases whose verified live recordings (2026-09-30) its wire shapes follow.
|
|
104
|
+
*/
|
|
105
|
+
export const gatewayEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [
|
|
106
|
+
{
|
|
107
|
+
method: 'POST',
|
|
108
|
+
path: gatewayChatCompletionsPath,
|
|
109
|
+
kind: 'provider',
|
|
110
|
+
write: false,
|
|
111
|
+
caseIds: [
|
|
112
|
+
'vercel-ai-gateway.stream.plain-text',
|
|
113
|
+
'vercel-ai-gateway.stream.deepseek-reasoning',
|
|
114
|
+
'vercel-ai-gateway.stream.tool-call-deltas',
|
|
115
|
+
'vercel-ai-gateway.stream.error-envelope'
|
|
116
|
+
],
|
|
117
|
+
evidence: 'verified',
|
|
118
|
+
observedAt: '2026-09-30'
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Route evidence manifest of the classifier route (served by the same fetch handler, with its own
|
|
124
|
+
* ledger, faults, and coverage). Its recordings are synthetic placeholders, so it is unverified.
|
|
125
|
+
*/
|
|
126
|
+
export const gatewayEvaluateEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [
|
|
127
|
+
{
|
|
128
|
+
method: 'POST',
|
|
129
|
+
path: gatewayEvaluatePath,
|
|
130
|
+
kind: 'provider',
|
|
131
|
+
write: false,
|
|
132
|
+
caseIds: [
|
|
133
|
+
'vercel-ai-gateway.classify.boolean',
|
|
134
|
+
'vercel-ai-gateway.classify.choice',
|
|
135
|
+
'vercel-ai-gateway.classify.score',
|
|
136
|
+
'vercel-ai-gateway.classify.error-envelope'
|
|
137
|
+
],
|
|
138
|
+
evidence: 'unverified',
|
|
139
|
+
observedAt: undefined
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
/** Optional fault filter; an omitted field matches every request. `path` ending in `*` is a prefix. */
|
|
144
|
+
export const GatewayFaultMatch = ChatFaultMatch
|
|
145
|
+
|
|
146
|
+
export type GatewayFaultMatch = ChatFaultMatch
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wire faults for emulated routes:
|
|
150
|
+
*
|
|
151
|
+
* - `status`: answer with this status, headers, and body instead of a
|
|
152
|
+
* completion (for example 429 with `retry-after`). The body defaults to a
|
|
153
|
+
* Gateway error envelope. Statuses that cannot carry a body (1xx, 204, 205,
|
|
154
|
+
* 304) and redirects (3xx) are rejected, as are invalid header names or
|
|
155
|
+
* values and a `location` header; scripted errors follow the same rules.
|
|
156
|
+
* - `error-after-chunks`: send `chunks` body chunks, then error the body
|
|
157
|
+
* stream (a dropped connection).
|
|
158
|
+
* - `truncate-after-chunks`: send `chunks` body chunks, then close the body
|
|
159
|
+
* cleanly (for example without `data: [DONE]`).
|
|
160
|
+
*
|
|
161
|
+
* Chunk faults count network chunks: a streamed chunk carries up to
|
|
162
|
+
* `eventsPerChunk` SSE events (default 2), and a whole JSON body counts as one
|
|
163
|
+
* chunk. A chunk fault that cannot take effect (`error-after-chunks` beyond
|
|
164
|
+
* the chunk count, or `truncate-after-chunks` at or beyond it) answers 500
|
|
165
|
+
* with an emulator error instead of silently doing nothing, and is not
|
|
166
|
+
* consumed.
|
|
167
|
+
*/
|
|
168
|
+
export const GatewayFault = ChatFault
|
|
169
|
+
|
|
170
|
+
export type GatewayFault = ChatFault
|
|
171
|
+
|
|
172
|
+
export type GatewayFaultKind = ChatFaultKind
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Usage for a scripted turn, sent as the Gateway `usage` object (token counts,
|
|
176
|
+
* `completion_tokens_details.reasoning_tokens` defaulting to 0, and synthetic
|
|
177
|
+
* zero costs).
|
|
178
|
+
*/
|
|
179
|
+
export const GatewayScriptedUsage = ChatScriptedUsage
|
|
180
|
+
|
|
181
|
+
export type GatewayScriptedUsage = ChatScriptedUsage
|
|
182
|
+
|
|
183
|
+
/** One scripted tool call; its JSON arguments stream as these fragments. */
|
|
184
|
+
export const GatewayScriptedToolCall = ChatScriptedToolCall
|
|
185
|
+
|
|
186
|
+
export type GatewayScriptedToolCall = ChatScriptedToolCall
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A scripted completion. Every field is exact (nothing is filled in) except:
|
|
190
|
+
* `usage` omitted is synthesized when the request asks for usage, and `null`
|
|
191
|
+
* drops it; `finishReason` omitted is `tool_calls` with tool calls, else
|
|
192
|
+
* `stop`. `order` defaults to `reasoning-first`; `reasoningField` defaults to
|
|
193
|
+
* `reasoning` (streamed with `reasoning_details`, as recorded live); the
|
|
194
|
+
* DeepSeek-native `reasoning_content` is sent alone.
|
|
195
|
+
*/
|
|
196
|
+
export const GatewayScriptedCompletion = ChatScriptedReasoningCompletion
|
|
197
|
+
|
|
198
|
+
export type GatewayScriptedCompletion = ChatScriptedReasoningCompletion
|
|
199
|
+
|
|
200
|
+
/** A scripted error response: status, body (a string is sent as is), and optional headers. */
|
|
201
|
+
export const GatewayScriptedError = ChatScriptedError
|
|
202
|
+
|
|
203
|
+
export type GatewayScriptedError = ChatScriptedError
|
|
204
|
+
|
|
205
|
+
/** A turn queued for the next chat completion request. */
|
|
206
|
+
export const GatewayScriptedTurn = ChatScriptedReasoningTurn
|
|
207
|
+
|
|
208
|
+
export type GatewayScriptedTurn = ChatScriptedReasoningTurn
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input, and
|
|
212
|
+
* by `makeGatewayEmulator` for invalid options; a programmer error.
|
|
213
|
+
*/
|
|
214
|
+
export class GatewayEmulatorInputInvalid extends Data.TaggedError('GatewayEmulatorInputInvalid')<{
|
|
215
|
+
readonly input: 'fault' | 'turn' | 'options'
|
|
216
|
+
readonly reason: string
|
|
217
|
+
}> {
|
|
218
|
+
override get message(): string {
|
|
219
|
+
return `Invalid Gateway emulator ${this.input}: ${this.reason}`
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type GatewayLedgerEntry = ChatLedgerEntry
|
|
224
|
+
|
|
225
|
+
export type GatewayFaultState = ChatFaultState
|
|
226
|
+
|
|
227
|
+
export type GatewayRouteCoverage = ChatRouteCoverage
|
|
228
|
+
|
|
229
|
+
export type GatewayCoverage = ChatCoverage
|
|
230
|
+
|
|
231
|
+
export type GatewayEmulatorOptions = {
|
|
232
|
+
/** Model ids that exist. Defaults to `gatewayEmulatorDefaultModels`. */
|
|
233
|
+
readonly knownModels?: ReadonlyArray<string>
|
|
234
|
+
/** Model ids that stream reasoning. Defaults to `gatewayEmulatorDefaultReasoningModels`. */
|
|
235
|
+
readonly reasoningModels?: ReadonlyArray<string>
|
|
236
|
+
/**
|
|
237
|
+
* SSE events per network chunk of a streamed body, a positive integer.
|
|
238
|
+
* Defaults to `gatewayEmulatorDefaultEventsPerChunk` (2); 1 sends one event
|
|
239
|
+
* per chunk. Events are packed counting from the end, so the last chunk
|
|
240
|
+
* holds the finish event and `data: [DONE]` together. Chunk faults count
|
|
241
|
+
* these network chunks.
|
|
242
|
+
*/
|
|
243
|
+
readonly eventsPerChunk?: number
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A classifier-route fault (`status` 400-599, `error-after-chunks`, `truncate-after-chunks`). */
|
|
247
|
+
export const GatewayEvaluateFault = FixtureRouteFault
|
|
248
|
+
|
|
249
|
+
export type GatewayEvaluateFault = FixtureRouteFault
|
|
250
|
+
|
|
251
|
+
export type GatewayEvaluateFaultKind = FixtureRouteFaultKind
|
|
252
|
+
|
|
253
|
+
/** A classifier-route turn: a scripted error (status 400-599) only. */
|
|
254
|
+
export type GatewayEvaluateScriptedTurn = FixtureRouteScriptedTurn
|
|
255
|
+
|
|
256
|
+
export type GatewayEvaluateLedgerEntry = FixtureRouteLedgerEntry
|
|
257
|
+
|
|
258
|
+
export type GatewayEvaluateEmulator = FixtureRouteEmulator
|
|
259
|
+
|
|
260
|
+
/** The chat emulator, plus `evaluate`: the classifier route's own emulator API. */
|
|
261
|
+
export type GatewayEmulator = ChatCompletionsEmulator<GatewayScriptedTurn> & {
|
|
262
|
+
readonly evaluate: GatewayEvaluateEmulator
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const gatewayErrorEnvelope = (error: ChatWireError): Schema.Json => ({
|
|
266
|
+
error: { message: error.message, type: error.type, code: error.code }
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Synthetic stand-ins for the metadata the live Gateway attaches to its chunks. Keys and value
|
|
270
|
+
// types follow the recordings; every value is synthetic (no recorded id, fingerprint, or cost).
|
|
271
|
+
const syntheticFingerprint = 'fp_synthetic'
|
|
272
|
+
|
|
273
|
+
const syntheticAttemptTime = 1790000000000
|
|
274
|
+
|
|
275
|
+
const syntheticCost = '0'
|
|
276
|
+
|
|
277
|
+
const vendorOf = (model: string): string | undefined => {
|
|
278
|
+
const [vendor] = model.split('/')
|
|
279
|
+
|
|
280
|
+
return vendor === undefined || vendor.length === 0 || vendor === model ? undefined : vendor
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// `service_tier` was recorded only for OpenAI models.
|
|
284
|
+
const isOpenAiModel = (model: string): boolean => vendorOf(model) === 'openai'
|
|
285
|
+
|
|
286
|
+
const syntheticResponseId = (identity: ChatResponseIdentity): string =>
|
|
287
|
+
`resp_synthetic_${identity.id}`
|
|
288
|
+
|
|
289
|
+
type GatewayUpstream = { readonly provider: string; readonly entry?: Schema.JsonObject }
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The upstream provider the Gateway routes a model to, and its `provider_metadata` entry (keyed by
|
|
293
|
+
* that provider), per model family as recorded: `openai/*` routes to `openai` with
|
|
294
|
+
* `{ responseId, serviceTier }`; `deepseek/*` routes to `baseten` with
|
|
295
|
+
* `{ acceptedPredictionTokens, rejectedPredictionTokens }` (synthetic zero counts). Other families
|
|
296
|
+
* were not recorded: they route to their vendor prefix without an upstream entry.
|
|
297
|
+
*/
|
|
298
|
+
const upstreamOf = (identity: ChatResponseIdentity): GatewayUpstream => {
|
|
299
|
+
const vendor = vendorOf(identity.model)
|
|
300
|
+
|
|
301
|
+
if (vendor === 'openai') {
|
|
302
|
+
return {
|
|
303
|
+
provider: 'openai',
|
|
304
|
+
entry: { responseId: syntheticResponseId(identity), serviceTier: 'default' }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (vendor === 'deepseek') {
|
|
309
|
+
return {
|
|
310
|
+
provider: 'baseten',
|
|
311
|
+
entry: { acceptedPredictionTokens: 0, rejectedPredictionTokens: 0 }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { provider: vendor ?? 'synthetic' }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The `gateway` entry of `provider_metadata`: routing, costs, and the generation id. */
|
|
319
|
+
const gatewayRoutingMetadata = (identity: ChatResponseIdentity): Schema.JsonObject => {
|
|
320
|
+
const { provider } = upstreamOf(identity)
|
|
321
|
+
const responseId = syntheticResponseId(identity)
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
routing: {
|
|
325
|
+
originalModelId: identity.model,
|
|
326
|
+
resolvedProvider: provider,
|
|
327
|
+
fallbacksAvailable: ['synthetic-fallback'],
|
|
328
|
+
planningReasoning: 'Synthetic: routing planned by the emulator.',
|
|
329
|
+
canonicalSlug: identity.model,
|
|
330
|
+
finalProvider: provider,
|
|
331
|
+
modelAttemptCount: 1,
|
|
332
|
+
modelAttempts: [
|
|
333
|
+
{
|
|
334
|
+
canonicalSlug: identity.model,
|
|
335
|
+
success: true,
|
|
336
|
+
providerAttemptCount: 1,
|
|
337
|
+
providerAttempts: [
|
|
338
|
+
{
|
|
339
|
+
provider,
|
|
340
|
+
credentialType: 'system',
|
|
341
|
+
success: true,
|
|
342
|
+
startTime: syntheticAttemptTime,
|
|
343
|
+
endTime: syntheticAttemptTime + 1,
|
|
344
|
+
providerRequestId: `req_synthetic_${identity.id}`,
|
|
345
|
+
statusCode: 200,
|
|
346
|
+
providerResponseId: responseId
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
totalProviderAttemptCount: 1,
|
|
352
|
+
affinity: { outcome: 'skipped_below_min_prefix' },
|
|
353
|
+
clientSessionId: 'synthetic-client-session',
|
|
354
|
+
clientSessionIdSource: 'fingerprint'
|
|
355
|
+
},
|
|
356
|
+
cost: syntheticCost,
|
|
357
|
+
marketCost: syntheticCost,
|
|
358
|
+
surchargeCost: syntheticCost,
|
|
359
|
+
gatewayCost: syntheticCost,
|
|
360
|
+
inferenceCost: syntheticCost,
|
|
361
|
+
inputInferenceCost: syntheticCost,
|
|
362
|
+
outputInferenceCost: syntheticCost,
|
|
363
|
+
generationId: identity.id
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** `provider_metadata`: the upstream provider's entry first (when its family has one), then `gateway`. */
|
|
368
|
+
const gatewayProviderMetadata = (identity: ChatResponseIdentity): Schema.JsonObject => {
|
|
369
|
+
const metadata: Record<string, Schema.Json> = {}
|
|
370
|
+
const upstream = upstreamOf(identity)
|
|
371
|
+
|
|
372
|
+
if (upstream.entry !== undefined) {
|
|
373
|
+
metadata[upstream.provider] = upstream.entry
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
metadata.gateway = gatewayRoutingMetadata(identity)
|
|
377
|
+
|
|
378
|
+
return metadata
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const gatewayUsage = (counts: ChatUsageCounts): Schema.JsonObject => ({
|
|
382
|
+
prompt_tokens: counts.promptTokens,
|
|
383
|
+
completion_tokens: counts.completionTokens,
|
|
384
|
+
total_tokens: counts.promptTokens + counts.completionTokens,
|
|
385
|
+
cost: 0,
|
|
386
|
+
is_byok: false,
|
|
387
|
+
prompt_tokens_details: { cached_tokens: 0, audio_tokens: 0, video_tokens: 0 },
|
|
388
|
+
cost_details: {
|
|
389
|
+
upstream_inference_cost: null,
|
|
390
|
+
upstream_inference_prompt_cost: 0,
|
|
391
|
+
upstream_inference_completions_cost: 0
|
|
392
|
+
},
|
|
393
|
+
completion_tokens_details: { reasoning_tokens: counts.reasoningTokens ?? 0, image_tokens: 0 },
|
|
394
|
+
cache_creation_input_tokens: 0,
|
|
395
|
+
market_cost: 0,
|
|
396
|
+
gateway_cost: 0
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const gatewayWireProfile = (eventsPerChunk: number): ChatWireProfile => ({
|
|
400
|
+
eventsPerChunk,
|
|
401
|
+
streamUsage: 'finish-event',
|
|
402
|
+
openingDelta: { role: 'assistant' },
|
|
403
|
+
choiceFields: { logprobs: null },
|
|
404
|
+
reasoningField: 'reasoning',
|
|
405
|
+
reasoningDetails: true,
|
|
406
|
+
chunkFields: () => ({ system_fingerprint: syntheticFingerprint }),
|
|
407
|
+
finishFields: identity => {
|
|
408
|
+
const chunk: Record<string, Schema.Json> = {}
|
|
409
|
+
|
|
410
|
+
if (isOpenAiModel(identity.model)) {
|
|
411
|
+
chunk.service_tier = 'default'
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
chunk.generationId = identity.id
|
|
415
|
+
|
|
416
|
+
return { delta: { provider_metadata: gatewayProviderMetadata(identity) }, chunk }
|
|
417
|
+
},
|
|
418
|
+
usage: gatewayUsage
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const validEventsPerChunk = (eventsPerChunk: number | undefined): number => {
|
|
422
|
+
const value = eventsPerChunk ?? gatewayEmulatorDefaultEventsPerChunk
|
|
423
|
+
|
|
424
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
425
|
+
throw new GatewayEmulatorInputInvalid({
|
|
426
|
+
input: 'options',
|
|
427
|
+
reason: `eventsPerChunk must be a positive integer, got ${String(eventsPerChunk)}`
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return value
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Create a Vercel AI Gateway emulator. Each call has independent ledger,
|
|
436
|
+
* fault, and script state. Throws `GatewayEmulatorInputInvalid` for an
|
|
437
|
+
* invalid `eventsPerChunk`.
|
|
438
|
+
*
|
|
439
|
+
* Without a script, `POST /v1/chat/completions` answers a known model with
|
|
440
|
+
* synthetic text deltas. `stream: true` sends `chat.completion.chunk` SSE in
|
|
441
|
+
* the recorded Gateway shape: a `{ role: 'assistant' }` opening delta, content
|
|
442
|
+
* deltas, and a finish event whose delta carries `provider_metadata` and which
|
|
443
|
+
* carries `usage` (when `stream_options.include_usage` is set),
|
|
444
|
+
* `system_fingerprint`, `service_tier` (for `openai/*` models), and
|
|
445
|
+
* `generationId`, then `data: [DONE]`; every chunk carries
|
|
446
|
+
* `system_fingerprint` and `logprobs: null`, and events are packed
|
|
447
|
+
* `eventsPerChunk` per network chunk. `stream: false` answers one
|
|
448
|
+
* `chat.completion` JSON body. Reasoning models asked for reasoning
|
|
449
|
+
* (`reasoning_effort`, or `thinking.type: 'enabled'`) stream
|
|
450
|
+
* `delta.reasoning` with `delta.reasoning_details` before the text. A request
|
|
451
|
+
* with `tools` gets one tool call whose arguments are synthesized from the
|
|
452
|
+
* tool's JSON Schema and streamed in fragments, finishing with `tool_calls`.
|
|
453
|
+
* Unknown models get the recorded 404 envelope (`type: 'model_not_found'`,
|
|
454
|
+
* `param: { modelId }`, no `code`); a missing bearer credential gets a 401
|
|
455
|
+
* envelope; unknown routes get a 404 JSON error. The ledger records the
|
|
456
|
+
* `max_tokens` limit as `maxCompletionTokens`.
|
|
457
|
+
*
|
|
458
|
+
* Precedence per chat request: authentication and JSON validation, then the
|
|
459
|
+
* first matching fault if it is a `status` fault, then the next scripted
|
|
460
|
+
* turn, then model validation and defaults; a first matching chunk fault then
|
|
461
|
+
* shapes the body. Only the first matching fault (in insertion order) applies. Credential headers are never
|
|
462
|
+
* recorded, and the bearer value is never checked or stored.
|
|
463
|
+
*
|
|
464
|
+
* `POST /v1/evaluate` is fixture-only: a request with a non-empty bearer credential (never checked
|
|
465
|
+
* or stored), the recorded `accept` and `content-type`, no query, and a body matching one of the
|
|
466
|
+
* four classifier recordings (the discriminators `model` and `type` exact; any other string; the
|
|
467
|
+
* same keys, so the recorded question ids, option keys, and level count) gets the recorded
|
|
468
|
+
* response; anything else (another model, `providerOptions`, other questions) answers 400
|
|
469
|
+
* not-emulated. Its ledger, faults (statuses 400-599), scripted errors, and coverage are
|
|
470
|
+
* `emulator.evaluate` (control plane `/_emulate/evaluate/*`); `emulator.faults` and the other
|
|
471
|
+
* top-level APIs stay the chat route's. `reset()` and `POST /_emulate/reset` reset both.
|
|
472
|
+
*/
|
|
473
|
+
export const makeGatewayEmulator = (options: GatewayEmulatorOptions = {}): GatewayEmulator => {
|
|
474
|
+
const chat = makeChatCompletionsEmulator({
|
|
475
|
+
path: gatewayChatCompletionsPath,
|
|
476
|
+
routes: gatewayEmulatorRoutes,
|
|
477
|
+
knownModels: options.knownModels ?? gatewayEmulatorDefaultModels,
|
|
478
|
+
reasoningModels: options.reasoningModels ?? gatewayEmulatorDefaultReasoningModels,
|
|
479
|
+
errorEnvelope: gatewayErrorEnvelope,
|
|
480
|
+
// Copied data shape of the verified Gateway error-envelope recording (unknown model id): its
|
|
481
|
+
// own envelope, with `param.modelId` and no `code`.
|
|
482
|
+
unknownModel: {
|
|
483
|
+
status: 404,
|
|
484
|
+
body: model => ({
|
|
485
|
+
error: {
|
|
486
|
+
message: `Model '${model ?? ''}' not found`,
|
|
487
|
+
type: 'model_not_found',
|
|
488
|
+
param: { modelId: model ?? null }
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
},
|
|
492
|
+
auth: {
|
|
493
|
+
unauthorized: {
|
|
494
|
+
message: 'Synthetic: missing or invalid authorization.',
|
|
495
|
+
type: 'authentication_error',
|
|
496
|
+
code: 'unauthorized'
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
completionTokenField: 'max_tokens',
|
|
500
|
+
responseIdPrefix: 'gen-synthetic',
|
|
501
|
+
defaultText: ['Hello', ' from the', ' synthetic gateway.'],
|
|
502
|
+
turnSchema: GatewayScriptedTurn,
|
|
503
|
+
inputInvalid: (input, reason) => new GatewayEmulatorInputInvalid({ input, reason }),
|
|
504
|
+
wire: gatewayWireProfile(validEventsPerChunk(options.eventsPerChunk))
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const evaluate = makeFixtureRouteEmulator({
|
|
508
|
+
method: 'POST',
|
|
509
|
+
path: gatewayEvaluatePath,
|
|
510
|
+
routes: gatewayEvaluateEmulatorRoutes,
|
|
511
|
+
recordings: gatewayEvaluateRecordings,
|
|
512
|
+
credential: 'bearer',
|
|
513
|
+
headers: [],
|
|
514
|
+
inputInvalid: (input, reason) => new GatewayEmulatorInputInvalid({ input, reason })
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
...chat,
|
|
519
|
+
fetch: composeFetch({
|
|
520
|
+
routes: [{ name: 'evaluate', paths: [gatewayEvaluatePath], part: evaluate }],
|
|
521
|
+
fallback: chat,
|
|
522
|
+
control: request => chat.fetch(request)
|
|
523
|
+
}),
|
|
524
|
+
reset: () => {
|
|
525
|
+
chat.reset()
|
|
526
|
+
evaluate.reset()
|
|
527
|
+
},
|
|
528
|
+
evaluate
|
|
529
|
+
}
|
|
530
|
+
}
|