@every-app/sdk 0.2.0 → 0.3.0-rc.10
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 +609 -3
- package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +174 -0
- package/dist/cloudflare/index.d.ts +23 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +24 -0
- package/dist/cloudflare/sentinel.d.ts +127 -0
- package/dist/cloudflare/sentinel.d.ts.map +1 -0
- package/dist/cloudflare/sentinel.js +85 -0
- package/dist/cloudflare/worker.d.ts +805 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +997 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +7 -0
- package/dist/dev/keypair.d.ts +26 -0
- package/dist/dev/keypair.d.ts.map +1 -0
- package/dist/dev/keypair.js +62 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/aiDefault.d.ts +204 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +334 -0
- package/dist/internal/aiProviders.d.ts +72 -0
- package/dist/internal/aiProviders.d.ts.map +1 -0
- package/dist/internal/aiProviders.js +233 -0
- package/dist/internal/appSettings.d.ts +315 -0
- package/dist/internal/appSettings.d.ts.map +1 -0
- package/dist/internal/appSettings.js +549 -0
- package/dist/internal/audit.d.ts +108 -0
- package/dist/internal/audit.d.ts.map +1 -0
- package/dist/internal/audit.js +235 -0
- package/dist/internal/capabilities.d.ts +3174 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +1110 -0
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +375 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +520 -0
- package/dist/internal/catalog/index.d.ts +1973 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +393 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +460 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +49 -0
- package/dist/internal/devProviders.d.ts.map +1 -0
- package/dist/internal/devProviders.js +80 -0
- package/dist/internal/email.d.ts +280 -0
- package/dist/internal/email.d.ts.map +1 -0
- package/dist/internal/email.js +613 -0
- package/dist/internal/events.d.ts +193 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +367 -0
- package/dist/internal/gatewaySettings.d.ts +83 -0
- package/dist/internal/gatewaySettings.d.ts.map +1 -0
- package/dist/internal/gatewaySettings.js +215 -0
- package/dist/internal/identity.d.ts +57 -5
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +99 -22
- package/dist/internal/index.d.ts +22 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +22 -1
- package/dist/internal/members.d.ts +36 -0
- package/dist/internal/members.d.ts.map +1 -0
- package/dist/internal/members.js +7 -0
- package/dist/internal/oauthClient.d.ts +61 -0
- package/dist/internal/oauthClient.d.ts.map +1 -0
- package/dist/internal/oauthClient.js +133 -0
- package/dist/internal/oauthPresets.d.ts +33 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +1064 -0
- package/dist/internal/oauthScopes.d.ts +172 -0
- package/dist/internal/oauthScopes.d.ts.map +1 -0
- package/dist/internal/oauthScopes.js +510 -0
- package/dist/internal/oauthSpec.d.ts +102 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +789 -0
- package/dist/internal/organizationName.d.ts +18 -0
- package/dist/internal/organizationName.d.ts.map +1 -0
- package/dist/internal/organizationName.js +24 -0
- package/dist/internal/protocol.d.ts +150 -3
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +163 -2
- package/dist/internal/providerApps.d.ts +75 -0
- package/dist/internal/providerApps.d.ts.map +1 -0
- package/dist/internal/providerApps.js +59 -0
- package/dist/internal/providerRegistry.d.ts +218 -0
- package/dist/internal/providerRegistry.d.ts.map +1 -0
- package/dist/internal/providerRegistry.js +432 -0
- package/dist/internal/providerSpec.d.ts +66 -0
- package/dist/internal/providerSpec.d.ts.map +1 -0
- package/dist/internal/providerSpec.js +1154 -0
- package/dist/internal/remoteMcp.d.ts +188 -0
- package/dist/internal/remoteMcp.d.ts.map +1 -0
- package/dist/internal/remoteMcp.js +543 -0
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +245 -0
- package/dist/internal/tools.d.ts +115 -0
- package/dist/internal/tools.d.ts.map +1 -0
- package/dist/internal/tools.js +80 -0
- package/dist/manifest.d.ts +238 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/server/ai.d.ts +111 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +145 -0
- package/dist/server/aiBinding.d.ts +68 -0
- package/dist/server/aiBinding.d.ts.map +1 -0
- package/dist/server/aiBinding.js +294 -0
- package/dist/server/attribution.d.ts +43 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +75 -0
- package/dist/server/authorizeUrl.d.ts +43 -0
- package/dist/server/authorizeUrl.d.ts.map +1 -0
- package/dist/server/authorizeUrl.js +106 -0
- package/dist/server/connectUrl.d.ts +21 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +37 -0
- package/dist/server/context.d.ts +68 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +117 -0
- package/dist/server/emailBinding.d.ts +72 -0
- package/dist/server/emailBinding.d.ts.map +1 -0
- package/dist/server/emailBinding.js +124 -0
- package/dist/server/emit.d.ts +63 -0
- package/dist/server/emit.d.ts.map +1 -0
- package/dist/server/emit.js +101 -0
- package/dist/server/env.d.ts +44 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +1 -0
- package/dist/server/everyApp.d.ts +65 -19
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +195 -30
- package/dist/server/fromMcpTools.d.ts +89 -0
- package/dist/server/fromMcpTools.d.ts.map +1 -0
- package/dist/server/fromMcpTools.js +192 -0
- package/dist/server/gatewayFetch.d.ts +54 -14
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +201 -37
- package/dist/server/gatewayUrl.d.ts +13 -0
- package/dist/server/gatewayUrl.d.ts.map +1 -0
- package/dist/server/gatewayUrl.js +34 -0
- package/dist/server/index.d.ts +23 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +19 -2
- package/dist/server/integrationFetch.d.ts +162 -0
- package/dist/server/integrationFetch.d.ts.map +1 -0
- package/dist/server/integrationFetch.js +688 -0
- package/dist/server/listMembers.d.ts +32 -0
- package/dist/server/listMembers.d.ts.map +1 -0
- package/dist/server/listMembers.js +71 -0
- package/dist/server/nodeServer.d.ts +48 -0
- package/dist/server/nodeServer.d.ts.map +1 -0
- package/dist/server/nodeServer.js +45 -0
- package/dist/server/refusal.d.ts +55 -0
- package/dist/server/refusal.d.ts.map +1 -0
- package/dist/server/refusal.js +193 -0
- package/dist/server/sendEmail.d.ts +39 -0
- package/dist/server/sendEmail.d.ts.map +1 -0
- package/dist/server/sendEmail.js +114 -0
- package/dist/server/tools.d.ts +184 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +347 -0
- package/dist/server/toolsEndpoint.d.ts +32 -0
- package/dist/server/toolsEndpoint.d.ts.map +1 -0
- package/dist/server/toolsEndpoint.js +237 -0
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.js +29 -0
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +156 -0
- package/package.json +42 -8
- package/dist/server/mcp.d.ts +0 -66
- package/dist/server/mcp.d.ts.map +0 -1
- package/dist/server/mcp.js +0 -147
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Gateway's default model provider: what an app that says "I need a
|
|
3
|
+
* chat model" (manifest `ai: { chat: true }`, the SDK's `ai()`, the route
|
|
4
|
+
* `/api/ai/default/…`) is served (docs/design/2026-10-model-providers.md).
|
|
5
|
+
*
|
|
6
|
+
* The default is resolved, never assumed: the operator's choice when one
|
|
7
|
+
* is stored (`everyapp ai default <provider>[/<model>]`, Manage workspace
|
|
8
|
+
* → Usage → AI default), else the shipped order below, first provider
|
|
9
|
+
* whose Gateway settings are all set; with none set, the first of the
|
|
10
|
+
* order with the settings it needs, so "Needs setup" asks for one key.
|
|
11
|
+
* OpenRouter comes first because one key reaches many models; Workers AI
|
|
12
|
+
* second because it needs no provider account (the Gateway's own
|
|
13
|
+
* Cloudflare token), with weaker models; then the single-vendor keys.
|
|
14
|
+
* Cloudflare AI Gateway is never a default: it has no models of its own
|
|
15
|
+
* and fronts whichever provider the default resolves to when
|
|
16
|
+
* `AI_GATEWAY_ID` is set (./capabilities.ts, `aig`).
|
|
17
|
+
*
|
|
18
|
+
* The models resolve the same way: the operator's model setting for the
|
|
19
|
+
* provider (`everyapp ai model <provider> --chat … --embeddings …`,
|
|
20
|
+
* `org_settings.ai_models`), else the catalog entry's seed (`chat.seedModel`,
|
|
21
|
+
* `embeddings.seedModel`). Nothing reads a seed except through here.
|
|
22
|
+
*
|
|
23
|
+
* Everything here is pure: what is set is asked of the caller (`isSet`),
|
|
24
|
+
* which reads the Gateway's env, the Worker's secret names, or `.dev.vars`.
|
|
25
|
+
*/
|
|
26
|
+
import { CATALOG, } from "./capabilities.js";
|
|
27
|
+
/** The reserved provider id apps call for the default: `/api/ai/default/…`. */
|
|
28
|
+
export const AI_DEFAULT_PROVIDER = "default";
|
|
29
|
+
/** The catalog id of Cloudflare AI Gateway, the proxy entry. */
|
|
30
|
+
export const AI_GATEWAY_PROVIDER = "ai-gateway";
|
|
31
|
+
/** The settings that make AI Gateway front the providers that have a slug. */
|
|
32
|
+
export const AI_GATEWAY_SETTINGS = [
|
|
33
|
+
"AI_GATEWAY_ID",
|
|
34
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* The shipped order: the provider the default resolves to when the
|
|
38
|
+
* operator chose none, the first of these whose settings are set.
|
|
39
|
+
*/
|
|
40
|
+
export const AI_DEFAULT_ORDER = [
|
|
41
|
+
"openrouter",
|
|
42
|
+
"workers-ai",
|
|
43
|
+
"openai",
|
|
44
|
+
"anthropic",
|
|
45
|
+
"groq",
|
|
46
|
+
"mistral",
|
|
47
|
+
"gemini",
|
|
48
|
+
];
|
|
49
|
+
/** The model name an app sends to mean "the default's model". */
|
|
50
|
+
export const AI_DEFAULT_MODEL = "default";
|
|
51
|
+
/** The paths an app calls under `/api/ai/default`, OpenAI-shaped. */
|
|
52
|
+
export const AI_DEFAULT_ROUTES = {
|
|
53
|
+
chat: "/v1/chat/completions",
|
|
54
|
+
embeddings: "/v1/embeddings",
|
|
55
|
+
};
|
|
56
|
+
/** Whether `value` can be a model id: 1 to 200 characters, no surrounding or control whitespace. */
|
|
57
|
+
export function isAiModelName(value) {
|
|
58
|
+
return (value.length >= 1 &&
|
|
59
|
+
value.length <= 200 &&
|
|
60
|
+
value.trim() === value &&
|
|
61
|
+
!/[\u0000-\u001f\u007f]/.test(value));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `org_settings.ai_models` as stored: a JSON object of provider id →
|
|
65
|
+
* `{ chat?, embeddings? }`. Anything else (null, malformed JSON, a field
|
|
66
|
+
* that isn't a model name) reads as no setting, so the seed serves.
|
|
67
|
+
*/
|
|
68
|
+
export function parseAiModelSettings(value) {
|
|
69
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
70
|
+
return {};
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(value);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
79
|
+
return {};
|
|
80
|
+
const settings = {};
|
|
81
|
+
for (const [provider, entry] of Object.entries(parsed)) {
|
|
82
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
83
|
+
continue;
|
|
84
|
+
const setting = {};
|
|
85
|
+
for (const kind of ["chat", "embeddings"]) {
|
|
86
|
+
const model = entry[kind];
|
|
87
|
+
if (typeof model === "string" && isAiModelName(model)) {
|
|
88
|
+
setting[kind] = model;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (setting.chat !== undefined || setting.embeddings !== undefined) {
|
|
92
|
+
settings[provider] = setting;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return settings;
|
|
96
|
+
}
|
|
97
|
+
/** The stored text of the settings, providers sorted; null when nothing is set. */
|
|
98
|
+
export function formatAiModelSettings(settings) {
|
|
99
|
+
const providers = Object.keys(settings)
|
|
100
|
+
.filter((id) => settings[id]?.chat !== undefined ||
|
|
101
|
+
settings[id]?.embeddings !== undefined)
|
|
102
|
+
.sort();
|
|
103
|
+
if (providers.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
return JSON.stringify(Object.fromEntries(providers.map((id) => {
|
|
106
|
+
const { chat, embeddings } = settings[id];
|
|
107
|
+
return [
|
|
108
|
+
id,
|
|
109
|
+
{
|
|
110
|
+
...(chat === undefined ? {} : { chat }),
|
|
111
|
+
...(embeddings === undefined ? {} : { embeddings }),
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
})));
|
|
115
|
+
}
|
|
116
|
+
/** `settings` with one provider's models changed by `patch`. */
|
|
117
|
+
export function withAiModelSetting(settings, provider, patch) {
|
|
118
|
+
const next = { ...settings[provider] };
|
|
119
|
+
for (const kind of ["chat", "embeddings"]) {
|
|
120
|
+
const value = patch[kind];
|
|
121
|
+
if (value === null)
|
|
122
|
+
delete next[kind];
|
|
123
|
+
else if (value !== undefined)
|
|
124
|
+
next[kind] = value;
|
|
125
|
+
}
|
|
126
|
+
const rest = Object.fromEntries(Object.entries(settings).filter(([id]) => id !== provider));
|
|
127
|
+
return next.chat === undefined && next.embeddings === undefined
|
|
128
|
+
? rest
|
|
129
|
+
: { ...rest, [provider]: next };
|
|
130
|
+
}
|
|
131
|
+
/** A choice must name a provider id; the model is free text after the first slash. */
|
|
132
|
+
const CHOICE = /^([a-z][a-z0-9-]{1,39})(?:\/(.{1,200}))?$/;
|
|
133
|
+
/**
|
|
134
|
+
* `openrouter` or `openrouter/anthropic/claude-haiku-5.5` as typed:
|
|
135
|
+
* the provider id up to the first slash, the model after it (a model id
|
|
136
|
+
* may hold slashes of its own). Null for blank or malformed text.
|
|
137
|
+
*/
|
|
138
|
+
export function parseAiDefaultChoice(value) {
|
|
139
|
+
if (typeof value !== "string")
|
|
140
|
+
return null;
|
|
141
|
+
const match = CHOICE.exec(value.trim());
|
|
142
|
+
if (!match)
|
|
143
|
+
return null;
|
|
144
|
+
const model = match[2]?.trim();
|
|
145
|
+
return model ? { provider: match[1], model } : { provider: match[1] };
|
|
146
|
+
}
|
|
147
|
+
/** The text of a choice: `provider` or `provider/model`. */
|
|
148
|
+
export function formatAiDefaultChoice(choice) {
|
|
149
|
+
return choice.model ? `${choice.provider}/${choice.model}` : choice.provider;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The provider id `org_settings.ai_default` holds, or null. The column
|
|
153
|
+
* holds the provider alone (its models are in `ai_models`); text after a
|
|
154
|
+
* first slash is not read.
|
|
155
|
+
*/
|
|
156
|
+
export function parseAiDefaultProvider(value) {
|
|
157
|
+
return parseAiDefaultChoice(value)?.provider ?? null;
|
|
158
|
+
}
|
|
159
|
+
export function canBeAiDefault(capability) {
|
|
160
|
+
return (capability?.mode === "ai" &&
|
|
161
|
+
capability.kind !== "proxy" &&
|
|
162
|
+
capability.chat !== undefined);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The Gateway settings an `ai` entry needs before it serves: its key
|
|
166
|
+
* (unless the entry works without one, `authOptional`) and what its URL
|
|
167
|
+
* needs besides (`alsoNeeds`).
|
|
168
|
+
*/
|
|
169
|
+
export function aiRequiredSettings(capability) {
|
|
170
|
+
return [
|
|
171
|
+
...(capability.authOptional ? [] : [capability.auth.secret]),
|
|
172
|
+
...(capability.alsoNeeds ?? []),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
/** Whether every setting the entry needs is set. */
|
|
176
|
+
export function aiProviderConfigured(capability, isSet) {
|
|
177
|
+
return aiRequiredSettings(capability).every((name) => isSet(name));
|
|
178
|
+
}
|
|
179
|
+
/** Whether AI Gateway fronts calls: its id and the account are set. */
|
|
180
|
+
export function aiGatewayConfigured(isSet) {
|
|
181
|
+
return AI_GATEWAY_SETTINGS.every((name) => isSet(name));
|
|
182
|
+
}
|
|
183
|
+
/** Whether a call to `capability` goes through AI Gateway: it is configured and the entry has a slug. */
|
|
184
|
+
export function viaAiGateway(capability, isSet) {
|
|
185
|
+
return capability.aig !== undefined && aiGatewayConfigured(isSet);
|
|
186
|
+
}
|
|
187
|
+
/** The provider's models: the operator's setting, else the seed. */
|
|
188
|
+
function modelsOf(id, capability, models) {
|
|
189
|
+
const setting = models[id];
|
|
190
|
+
const { chat, embeddings } = capability;
|
|
191
|
+
return {
|
|
192
|
+
model: setting?.chat ?? chat.seedModel,
|
|
193
|
+
...(embeddings
|
|
194
|
+
? { embeddingsModel: setting?.embeddings ?? embeddings.seedModel }
|
|
195
|
+
: {}),
|
|
196
|
+
seed: {
|
|
197
|
+
chat: chat.seedModel,
|
|
198
|
+
...(embeddings ? { embeddings: embeddings.seedModel } : {}),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function candidateOf(id, capability, isSet, models) {
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
label: capability.label,
|
|
206
|
+
settings: aiRequiredSettings(capability),
|
|
207
|
+
configured: aiProviderConfigured(capability, isSet),
|
|
208
|
+
...modelsOf(id, capability, models),
|
|
209
|
+
viaAiGateway: viaAiGateway(capability, isSet),
|
|
210
|
+
embeddings: capability.embeddings !== undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Every provider the default may resolve to: the shipped order first,
|
|
215
|
+
* then any other `ai` entry of `lookup` with a chat route (a registered
|
|
216
|
+
* row, one day), each with its state.
|
|
217
|
+
*/
|
|
218
|
+
export function aiDefaultCandidates(isSet, lookup = CATALOG, models = {}) {
|
|
219
|
+
const ids = [
|
|
220
|
+
...AI_DEFAULT_ORDER,
|
|
221
|
+
...lookup.ids().filter((id) => !AI_DEFAULT_ORDER.includes(id)),
|
|
222
|
+
];
|
|
223
|
+
return ids.flatMap((id) => {
|
|
224
|
+
const capability = lookup.get(id);
|
|
225
|
+
// A registered row whose key isn't bound to the hosts it has now reads
|
|
226
|
+
// as unset everywhere (`providers.value-bound-to-hosts`).
|
|
227
|
+
const set = lookup.bound && !lookup.bound(id) ? () => false : isSet;
|
|
228
|
+
return canBeAiDefault(capability)
|
|
229
|
+
? [candidateOf(id, capability, set, models)]
|
|
230
|
+
: [];
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** Why `choice.provider` can't be the default, or null when it can. */
|
|
234
|
+
export function aiDefaultChoiceProblem(choice, lookup = CATALOG) {
|
|
235
|
+
const capability = lookup.get(choice.provider);
|
|
236
|
+
if (!capability)
|
|
237
|
+
return `${choice.provider} isn't a provider this Gateway knows`;
|
|
238
|
+
if (capability.mode !== "ai") {
|
|
239
|
+
return `${choice.provider} is an API provider, not a model provider`;
|
|
240
|
+
}
|
|
241
|
+
if (capability.kind === "proxy") {
|
|
242
|
+
return `${capability.label} fronts the model providers and serves no models of its own; set a provider, and turn the gateway on with everyapp ai gateway on`;
|
|
243
|
+
}
|
|
244
|
+
if (!capability.chat) {
|
|
245
|
+
return `${capability.label} has no OpenAI-shaped chat route, so it can't serve the default; an app pins it by listing it under providers`;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Why `patch` can't be stored for `provider`, or null when it can: the
|
|
251
|
+
* provider must be one the default can be, a model name must be a model
|
|
252
|
+
* name, and an embeddings model needs the entry's embeddings route.
|
|
253
|
+
*/
|
|
254
|
+
export function aiModelPatchProblem(provider, patch, lookup = CATALOG) {
|
|
255
|
+
const why = aiDefaultChoiceProblem({ provider }, lookup);
|
|
256
|
+
if (why)
|
|
257
|
+
return why;
|
|
258
|
+
const capability = lookup.get(provider);
|
|
259
|
+
if (!canBeAiDefault(capability))
|
|
260
|
+
return `${provider} can't serve the default`;
|
|
261
|
+
for (const kind of ["chat", "embeddings"]) {
|
|
262
|
+
const value = patch[kind];
|
|
263
|
+
if (typeof value === "string" && !isAiModelName(value)) {
|
|
264
|
+
return `${JSON.stringify(value)} isn't a model id: 1 to 200 characters, no surrounding spaces`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (typeof patch.embeddings === "string" && !capability.embeddings) {
|
|
268
|
+
return `${capability.label} has no embeddings route, so it serves no embeddings model`;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The Gateway's effective default, from the stored choice (or none) and
|
|
274
|
+
* what is set. A choice that names a provider which isn't configured
|
|
275
|
+
* still wins (the operator chose it: `configured` is false and `settings`
|
|
276
|
+
* says what to set); a choice the default can't be is reported as
|
|
277
|
+
* `problem` and the order decides.
|
|
278
|
+
*/
|
|
279
|
+
export function resolveAiDefault(input) {
|
|
280
|
+
const lookup = input.lookup ?? CATALOG;
|
|
281
|
+
const models = input.models ?? {};
|
|
282
|
+
const { isSet } = input;
|
|
283
|
+
const candidates = aiDefaultCandidates(isSet, lookup, models);
|
|
284
|
+
const resolve = (id, capability, source, problem) => ({
|
|
285
|
+
provider: id,
|
|
286
|
+
label: capability.label,
|
|
287
|
+
...modelsOf(id, capability, models),
|
|
288
|
+
source,
|
|
289
|
+
configured: aiProviderConfigured(capability, isSet),
|
|
290
|
+
settings: aiRequiredSettings(capability),
|
|
291
|
+
via: viaAiGateway(capability, isSet) ? "ai-gateway" : "direct",
|
|
292
|
+
chat: capability.chat,
|
|
293
|
+
...(capability.embeddings ? { embeddings: capability.embeddings } : {}),
|
|
294
|
+
candidates: [
|
|
295
|
+
...candidates.filter((candidate) => candidate.id === id),
|
|
296
|
+
...candidates.filter((candidate) => candidate.id !== id),
|
|
297
|
+
],
|
|
298
|
+
...(problem ? { problem } : {}),
|
|
299
|
+
});
|
|
300
|
+
let problem;
|
|
301
|
+
if (input.provider) {
|
|
302
|
+
const capability = lookup.get(input.provider);
|
|
303
|
+
const why = aiDefaultChoiceProblem({ provider: input.provider }, lookup);
|
|
304
|
+
if (!why && canBeAiDefault(capability)) {
|
|
305
|
+
return resolve(input.provider, capability, "setting");
|
|
306
|
+
}
|
|
307
|
+
problem = why ?? undefined;
|
|
308
|
+
}
|
|
309
|
+
const configured = candidates.find((candidate) => candidate.configured);
|
|
310
|
+
const first = configured ?? candidates[0];
|
|
311
|
+
if (!first) {
|
|
312
|
+
throw new Error("the catalog has no AI provider with a chat route");
|
|
313
|
+
}
|
|
314
|
+
const capability = lookup.get(first.id);
|
|
315
|
+
if (!canBeAiDefault(capability))
|
|
316
|
+
throw new Error("unreachable");
|
|
317
|
+
return resolve(first.id, capability, configured ? "order" : "none", problem);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The sentence setup shows for an unconfigured default: the one key to
|
|
321
|
+
* set, and the alternatives in the order.
|
|
322
|
+
*/
|
|
323
|
+
export function aiDefaultSetupHint(resolution) {
|
|
324
|
+
const others = resolution.candidates
|
|
325
|
+
.filter((candidate) => candidate.id !== resolution.provider)
|
|
326
|
+
.slice(0, 3)
|
|
327
|
+
.map((candidate) => `${candidate.label} (${candidate.settings.join(" and ")})`);
|
|
328
|
+
const head = resolution.source === "setting"
|
|
329
|
+
? `The Gateway's default model provider is ${resolution.label}; set ${resolution.settings.join(" and ")}.`
|
|
330
|
+
: `Any one model provider's key makes the app work; ${resolution.label} is first in the order: set ${resolution.settings.join(" and ")}.`;
|
|
331
|
+
return others.length > 0
|
|
332
|
+
? `${head} Or another: ${others.join(", ")}; pick one with everyapp ai default <provider>.`
|
|
333
|
+
: head;
|
|
334
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's AI providers as the SDK's local-dev fallback reaches them:
|
|
3
|
+
* with no gateway binding, a developer's own key, under the name of the
|
|
4
|
+
* gateway secret that holds it, goes to the provider directly. Deployed
|
|
5
|
+
* apps always go through the gateway, which holds the real keys. The
|
|
6
|
+
* providers themselves are the catalog's `ai` entries (./capabilities.ts);
|
|
7
|
+
* this derives the fallback's view of each, and holds the one response
|
|
8
|
+
* rewrite the gateway has (Workers AI's numeric tokens).
|
|
9
|
+
*/
|
|
10
|
+
import { type AiProviderCapability, type AiProviderId, type AiRoute, type ApiKeyAuth, type CapabilitySettings } from "./capabilities.js";
|
|
11
|
+
export interface DevAiProviderSpec {
|
|
12
|
+
origin: string;
|
|
13
|
+
/** The routes the default maps onto in dev, with the model filled in (the entry's `chat` and `embeddings`). */
|
|
14
|
+
chat?: AiRoute;
|
|
15
|
+
embeddings?: AiRoute;
|
|
16
|
+
/** The gateway secret holding the provider key; in local dev, the `.dev.vars` name. */
|
|
17
|
+
keyEnv: string;
|
|
18
|
+
/** Where the key goes on a request (`authorizationOf`). */
|
|
19
|
+
auth: ApiKeyAuth;
|
|
20
|
+
/** Where an operator gets the key: the provider's API keys page. */
|
|
21
|
+
docs: string;
|
|
22
|
+
/** The everyapp skill's setup guide for it, relative to the skill (`setup/openai.md`). */
|
|
23
|
+
setup?: string;
|
|
24
|
+
/** Gateway settings the provider's URL needs besides the key, under the same names. */
|
|
25
|
+
alsoNeeds?: readonly string[];
|
|
26
|
+
/** The path every provider path is joined onto, from those settings. */
|
|
27
|
+
basePath?: (settings: Readonly<Record<string, string>>) => string;
|
|
28
|
+
/** Turns the provider's response at `path` into what OpenAI clients parse. */
|
|
29
|
+
normalizeResponse?: (path: string, response: Response) => Response;
|
|
30
|
+
}
|
|
31
|
+
export type DevAiProvider = AiProviderId;
|
|
32
|
+
/** The fallback's view of one `ai` catalog entry. */
|
|
33
|
+
export declare function devAiProviderSpec(capability: AiProviderCapability): DevAiProviderSpec;
|
|
34
|
+
export declare const DEV_AI_PROVIDERS: Readonly<Record<DevAiProvider, DevAiProviderSpec>>;
|
|
35
|
+
export declare function devAiProvider(name: string,
|
|
36
|
+
/** The dev Worker's bindings: a registered `ai` spec under `.everyapp/providers/<id>.json` resolves from them. */
|
|
37
|
+
values?: Readonly<Record<string, unknown>>): DevAiProviderSpec | undefined;
|
|
38
|
+
/** Every setting a provider needs, its key first: the names `.dev.vars` and the gateway use. */
|
|
39
|
+
export declare function aiProviderSettings(spec: DevAiProviderSpec): string[];
|
|
40
|
+
/**
|
|
41
|
+
* An `ai` entry's `basePath` with each `{SETTING}` filled from the
|
|
42
|
+
* settings, URL-encoded (`/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai`
|
|
43
|
+
* with `acc 1` makes `/client/v4/accounts/acc%201/ai`), or null while a
|
|
44
|
+
* named setting is unset or blank.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fillBasePath(template: string, settings: CapabilitySettings): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* The base URL an `ai` entry's calls are joined onto, from the Gateway's
|
|
49
|
+
* settings: its origin and filled `basePath`, or null while a setting the
|
|
50
|
+
* path needs is unset.
|
|
51
|
+
*/
|
|
52
|
+
export declare function aiBaseUrl(capability: AiProviderCapability, settings: CapabilitySettings): string | null;
|
|
53
|
+
/** The account-scoped base path of Workers AI's REST API. */
|
|
54
|
+
export declare function workersAiBasePath(accountId: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* `base` with `path` joined onto its path (never replacing it) and
|
|
57
|
+
* `search` as its query: `https://api.cloudflare.com/client/v4/accounts/<id>/ai`
|
|
58
|
+
* and `/v1/chat/completions` make `…/ai/v1/chat/completions`. `path` starts
|
|
59
|
+
* with one slash; null when the result would leave `base`'s path.
|
|
60
|
+
*/
|
|
61
|
+
export declare function joinProviderUrl(base: string, path: string, search?: string): URL | null;
|
|
62
|
+
/**
|
|
63
|
+
* Workers AI's OpenAI-compatible chat completions on the vLLM-backed models
|
|
64
|
+
* (Meta's Llama among them) send a token made only of digits as a JSON
|
|
65
|
+
* number (`"delta":{"content":18}`), which OpenAI clients refuse:
|
|
66
|
+
* `@ai-sdk/openai-compatible` drops the token and reports an error part.
|
|
67
|
+
* This turns such numbers back into strings, in `choices[].delta` (streams)
|
|
68
|
+
* and `choices[].message` (whole JSON bodies); every other event or body
|
|
69
|
+
* passes through byte for byte. Remove it once Cloudflare sends strings.
|
|
70
|
+
*/
|
|
71
|
+
export declare function normalizeWorkersAiResponse(path: string, response: Response): Response;
|
|
72
|
+
//# sourceMappingURL=aiProviders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiProviders.d.ts","sourceRoot":"","sources":["../../src/internal/aiProviders.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAG3B,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,+GAA+G;IAC/G,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,IAAI,EAAE,UAAU,CAAC;IACjB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC;IAClE,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC;CACpE;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAEzC,qDAAqD;AACrD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,oBAAoB,GAC/B,iBAAiB,CAoBnB;AAED,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CACrC,MAAM,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAUG,CAAC;AAE9C,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM;AACZ,kHAAkH;AAClH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACzC,iBAAiB,GAAG,SAAS,CAS/B;AAED,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,GAAG,IAAI,CAcf;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,oBAAoB,EAChC,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,SAAK,GACV,GAAG,GAAG,IAAI,CAOZ;AAWD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GACjB,QAAQ,CAqBV"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's AI providers as the SDK's local-dev fallback reaches them:
|
|
3
|
+
* with no gateway binding, a developer's own key, under the name of the
|
|
4
|
+
* gateway secret that holds it, goes to the provider directly. Deployed
|
|
5
|
+
* apps always go through the gateway, which holds the real keys. The
|
|
6
|
+
* providers themselves are the catalog's `ai` entries (./capabilities.ts);
|
|
7
|
+
* this derives the fallback's view of each, and holds the one response
|
|
8
|
+
* rewrite the gateway has (Workers AI's numeric tokens).
|
|
9
|
+
*/
|
|
10
|
+
import { CAPABILITIES, CAPABILITY_IDS, } from "./capabilities.js";
|
|
11
|
+
import { resolveDevProvider } from "./devProviders.js";
|
|
12
|
+
/** The fallback's view of one `ai` catalog entry. */
|
|
13
|
+
export function devAiProviderSpec(capability) {
|
|
14
|
+
const { basePath, normalize } = capability;
|
|
15
|
+
return {
|
|
16
|
+
origin: capability.origin,
|
|
17
|
+
...(capability.chat ? { chat: capability.chat } : {}),
|
|
18
|
+
...(capability.embeddings ? { embeddings: capability.embeddings } : {}),
|
|
19
|
+
keyEnv: capability.auth.secret,
|
|
20
|
+
auth: capability.auth,
|
|
21
|
+
docs: capability.docs,
|
|
22
|
+
...(capability.setup === undefined ? {} : { setup: capability.setup }),
|
|
23
|
+
...(capability.alsoNeeds === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { alsoNeeds: capability.alsoNeeds }),
|
|
26
|
+
...(basePath === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { basePath: (settings) => fillBasePath(basePath, settings) ?? "" }),
|
|
29
|
+
...(normalize === "workers-ai"
|
|
30
|
+
? { normalizeResponse: normalizeWorkersAiResponse }
|
|
31
|
+
: {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export const DEV_AI_PROVIDERS = Object.fromEntries(CAPABILITY_IDS.flatMap((id) => {
|
|
35
|
+
const capability = CAPABILITIES[id];
|
|
36
|
+
// The proxy entry (AI Gateway) serves no models: never a dev target.
|
|
37
|
+
return capability.mode === "ai" &&
|
|
38
|
+
!("kind" in capability && capability.kind === "proxy")
|
|
39
|
+
? [[id, devAiProviderSpec(capability)]]
|
|
40
|
+
: [];
|
|
41
|
+
}));
|
|
42
|
+
export function devAiProvider(name,
|
|
43
|
+
/** The dev Worker's bindings: a registered `ai` spec under `.everyapp/providers/<id>.json` resolves from them. */
|
|
44
|
+
values) {
|
|
45
|
+
if (Object.hasOwn(DEV_AI_PROVIDERS, name)) {
|
|
46
|
+
return DEV_AI_PROVIDERS[name];
|
|
47
|
+
}
|
|
48
|
+
if (!values)
|
|
49
|
+
return undefined;
|
|
50
|
+
const resolved = resolveDevProvider(values, name);
|
|
51
|
+
return resolved.found && resolved.capability.mode === "ai"
|
|
52
|
+
? devAiProviderSpec(resolved.capability)
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Every setting a provider needs, its key first: the names `.dev.vars` and the gateway use. */
|
|
56
|
+
export function aiProviderSettings(spec) {
|
|
57
|
+
return [spec.keyEnv, ...(spec.alsoNeeds ?? [])];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An `ai` entry's `basePath` with each `{SETTING}` filled from the
|
|
61
|
+
* settings, URL-encoded (`/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai`
|
|
62
|
+
* with `acc 1` makes `/client/v4/accounts/acc%201/ai`), or null while a
|
|
63
|
+
* named setting is unset or blank.
|
|
64
|
+
*/
|
|
65
|
+
export function fillBasePath(template, settings) {
|
|
66
|
+
let missing = false;
|
|
67
|
+
const filled = template.replace(/\{([A-Z][A-Z0-9_]*)\}/g, (_, name) => {
|
|
68
|
+
const value = settings[name];
|
|
69
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
70
|
+
missing = true;
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
return encodeURIComponent(value.trim());
|
|
74
|
+
});
|
|
75
|
+
return missing ? null : filled;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The base URL an `ai` entry's calls are joined onto, from the Gateway's
|
|
79
|
+
* settings: its origin and filled `basePath`, or null while a setting the
|
|
80
|
+
* path needs is unset.
|
|
81
|
+
*/
|
|
82
|
+
export function aiBaseUrl(capability, settings) {
|
|
83
|
+
if (capability.basePath === undefined)
|
|
84
|
+
return capability.origin;
|
|
85
|
+
const path = fillBasePath(capability.basePath, settings);
|
|
86
|
+
return path === null ? null : `${capability.origin}${path}`;
|
|
87
|
+
}
|
|
88
|
+
/** The account-scoped base path of Workers AI's REST API. */
|
|
89
|
+
export function workersAiBasePath(accountId) {
|
|
90
|
+
return `/client/v4/accounts/${encodeURIComponent(accountId.trim())}/ai`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* `base` with `path` joined onto its path (never replacing it) and
|
|
94
|
+
* `search` as its query: `https://api.cloudflare.com/client/v4/accounts/<id>/ai`
|
|
95
|
+
* and `/v1/chat/completions` make `…/ai/v1/chat/completions`. `path` starts
|
|
96
|
+
* with one slash; null when the result would leave `base`'s path.
|
|
97
|
+
*/
|
|
98
|
+
export function joinProviderUrl(base, path, search = "") {
|
|
99
|
+
const url = new URL(base);
|
|
100
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
101
|
+
url.pathname = `${basePath}${path}`;
|
|
102
|
+
url.search = search;
|
|
103
|
+
url.hash = "";
|
|
104
|
+
return url.pathname === `${basePath}${path}` ? url : null;
|
|
105
|
+
}
|
|
106
|
+
const WORKERS_AI_NORMALIZED_ROUTES = new Set([
|
|
107
|
+
"/v1/chat/completions",
|
|
108
|
+
]);
|
|
109
|
+
/** Text fields of a chat completion choice that OpenAI clients require as strings. */
|
|
110
|
+
const TEXT_FIELDS = ["content", "reasoning_content", "reasoning"];
|
|
111
|
+
const NUMERIC_TEXT_FIELD = /"(?:content|reasoning_content|reasoning)"\s*:\s*-?\d/;
|
|
112
|
+
/**
|
|
113
|
+
* Workers AI's OpenAI-compatible chat completions on the vLLM-backed models
|
|
114
|
+
* (Meta's Llama among them) send a token made only of digits as a JSON
|
|
115
|
+
* number (`"delta":{"content":18}`), which OpenAI clients refuse:
|
|
116
|
+
* `@ai-sdk/openai-compatible` drops the token and reports an error part.
|
|
117
|
+
* This turns such numbers back into strings, in `choices[].delta` (streams)
|
|
118
|
+
* and `choices[].message` (whole JSON bodies); every other event or body
|
|
119
|
+
* passes through byte for byte. Remove it once Cloudflare sends strings.
|
|
120
|
+
*/
|
|
121
|
+
export function normalizeWorkersAiResponse(path, response) {
|
|
122
|
+
if (!response.body || !WORKERS_AI_NORMALIZED_ROUTES.has(path)) {
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
125
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
126
|
+
const transform = contentType.startsWith("text/event-stream")
|
|
127
|
+
? normalizeSseLines()
|
|
128
|
+
: contentType.startsWith("application/json")
|
|
129
|
+
? normalizeJsonBody()
|
|
130
|
+
: null;
|
|
131
|
+
if (!transform)
|
|
132
|
+
return response;
|
|
133
|
+
const headers = new Headers(response.headers);
|
|
134
|
+
// The body may change length.
|
|
135
|
+
headers.delete("content-length");
|
|
136
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
137
|
+
status: response.status,
|
|
138
|
+
statusText: response.statusText,
|
|
139
|
+
headers,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Stringify numeric text fields of every choice in a chat completion (chunk). */
|
|
143
|
+
function stringifyNumericText(payload) {
|
|
144
|
+
if (!isRecord(payload) || !Array.isArray(payload["choices"]))
|
|
145
|
+
return false;
|
|
146
|
+
let changed = false;
|
|
147
|
+
for (const choice of payload["choices"]) {
|
|
148
|
+
if (!isRecord(choice))
|
|
149
|
+
continue;
|
|
150
|
+
for (const part of [choice["delta"], choice["message"]]) {
|
|
151
|
+
if (!isRecord(part))
|
|
152
|
+
continue;
|
|
153
|
+
for (const field of TEXT_FIELDS) {
|
|
154
|
+
const value = part[field];
|
|
155
|
+
if (typeof value === "number") {
|
|
156
|
+
part[field] = String(value);
|
|
157
|
+
changed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return changed;
|
|
163
|
+
}
|
|
164
|
+
/** `json` with its numeric text fields as strings, or null when it has none. */
|
|
165
|
+
function normalizedJson(json) {
|
|
166
|
+
if (!NUMERIC_TEXT_FIELD.test(json))
|
|
167
|
+
return null;
|
|
168
|
+
let payload;
|
|
169
|
+
try {
|
|
170
|
+
payload = JSON.parse(json);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return stringifyNumericText(payload) ? JSON.stringify(payload) : null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* An SSE stream, line by line: a `data:` line whose JSON has a numeric text
|
|
179
|
+
* field is rewritten, every other line (and its line ending) is kept.
|
|
180
|
+
*/
|
|
181
|
+
function normalizeSseLines() {
|
|
182
|
+
const decoder = new TextDecoder();
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
let pending = "";
|
|
185
|
+
const normalizeLine = (line) => {
|
|
186
|
+
const match = /^(data:[ ]?)(.*?)(\r?)$/s.exec(line);
|
|
187
|
+
if (!match)
|
|
188
|
+
return line;
|
|
189
|
+
const rewritten = normalizedJson(match[2]);
|
|
190
|
+
return rewritten === null ? line : `${match[1]}${rewritten}${match[3]}`;
|
|
191
|
+
};
|
|
192
|
+
return new TransformStream({
|
|
193
|
+
transform(chunk, controller) {
|
|
194
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
195
|
+
const end = pending.lastIndexOf("\n");
|
|
196
|
+
if (end === -1)
|
|
197
|
+
return;
|
|
198
|
+
const complete = pending.slice(0, end + 1);
|
|
199
|
+
pending = pending.slice(end + 1);
|
|
200
|
+
controller.enqueue(encoder.encode(complete.split("\n").map(normalizeLine).join("\n")));
|
|
201
|
+
},
|
|
202
|
+
flush(controller) {
|
|
203
|
+
pending += decoder.decode();
|
|
204
|
+
if (pending)
|
|
205
|
+
controller.enqueue(encoder.encode(normalizeLine(pending)));
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** A whole JSON body: rewritten when it has a numeric text field, else kept as sent. */
|
|
210
|
+
function normalizeJsonBody() {
|
|
211
|
+
const chunks = [];
|
|
212
|
+
return new TransformStream({
|
|
213
|
+
transform(chunk) {
|
|
214
|
+
chunks.push(chunk);
|
|
215
|
+
},
|
|
216
|
+
flush(controller) {
|
|
217
|
+
const decoder = new TextDecoder();
|
|
218
|
+
const text = chunks
|
|
219
|
+
.map((chunk) => decoder.decode(chunk, { stream: true }))
|
|
220
|
+
.join("") + decoder.decode();
|
|
221
|
+
const rewritten = normalizedJson(text);
|
|
222
|
+
if (rewritten !== null) {
|
|
223
|
+
controller.enqueue(new TextEncoder().encode(rewritten));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const chunk of chunks)
|
|
227
|
+
controller.enqueue(chunk);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function isRecord(value) {
|
|
232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
233
|
+
}
|