@x12i/ai-dispatcher 1.4.0 → 2.0.0
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/README.md +160 -31
- package/dist/index.cjs +2118 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -7
- package/dist/index.d.ts +157 -7
- package/dist/index.js +2127 -200
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { decodeProviderMetadata } from "@x12i/openrouter-runtime";
|
|
3
|
+
|
|
4
|
+
// src/cache-usage.ts
|
|
5
|
+
function readCacheTokens(source) {
|
|
6
|
+
const cached = [];
|
|
7
|
+
const written = [];
|
|
8
|
+
for (const usage of usageRecords(source)) {
|
|
9
|
+
const promptDetails = record(usage.prompt_tokens_details);
|
|
10
|
+
const inputDetails = record(usage.input_tokens_details);
|
|
11
|
+
pushNumber(cached, promptDetails?.cached_tokens);
|
|
12
|
+
pushNumber(cached, inputDetails?.cached_tokens);
|
|
13
|
+
pushNumber(cached, usage.cacheReadInputTokens);
|
|
14
|
+
pushNumber(cached, usage.cache_read_input_tokens);
|
|
15
|
+
pushNumber(cached, usage.cached_tokens);
|
|
16
|
+
pushNumber(written, promptDetails?.cache_write_tokens);
|
|
17
|
+
pushNumber(written, inputDetails?.cache_write_tokens);
|
|
18
|
+
pushNumber(written, usage.cacheWriteInputTokens);
|
|
19
|
+
pushNumber(written, usage.cache_write_input_tokens);
|
|
20
|
+
pushNumber(written, usage.cache_write_tokens);
|
|
21
|
+
}
|
|
22
|
+
const cachedTokens = cached[0];
|
|
23
|
+
const cacheWriteTokens = written[0];
|
|
24
|
+
return {
|
|
25
|
+
...cachedTokens !== void 0 ? { cachedTokens } : {},
|
|
26
|
+
...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function usageRecords(source) {
|
|
30
|
+
const root = record(source);
|
|
31
|
+
if (!root) return [];
|
|
32
|
+
const records = [];
|
|
33
|
+
const usage = record(root.usage);
|
|
34
|
+
if (usage) records.push(usage);
|
|
35
|
+
const raw = record(root.raw);
|
|
36
|
+
if (raw) {
|
|
37
|
+
const rawUsage = record(raw.usage);
|
|
38
|
+
records.push(rawUsage ?? raw);
|
|
39
|
+
}
|
|
40
|
+
records.push(root);
|
|
41
|
+
return records;
|
|
42
|
+
}
|
|
43
|
+
function pushNumber(target, value) {
|
|
44
|
+
if (typeof value === "number" && Number.isFinite(value)) target.push(value);
|
|
45
|
+
}
|
|
46
|
+
function record(value) {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
1
50
|
// src/json-pointer.ts
|
|
2
51
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3
52
|
function isRecord(value) {
|
|
@@ -139,7 +188,7 @@ var ThinkTagSplitter = class {
|
|
|
139
188
|
push(chunk) {
|
|
140
189
|
this.pending += chunk;
|
|
141
190
|
let answerDelta = "";
|
|
142
|
-
let
|
|
191
|
+
let reasoningDelta2 = "";
|
|
143
192
|
while (this.pending.length > 0) {
|
|
144
193
|
if (!this.inThink) {
|
|
145
194
|
const openAt = this.pending.indexOf(OPEN);
|
|
@@ -156,26 +205,26 @@ var ThinkTagSplitter = class {
|
|
|
156
205
|
}
|
|
157
206
|
const closeAt = this.pending.indexOf(CLOSE);
|
|
158
207
|
if (closeAt >= 0) {
|
|
159
|
-
|
|
208
|
+
reasoningDelta2 += this.pending.slice(0, closeAt);
|
|
160
209
|
this.pending = this.pending.slice(closeAt + CLOSE.length);
|
|
161
210
|
this.inThink = false;
|
|
162
211
|
continue;
|
|
163
212
|
}
|
|
164
213
|
const hold = holdSuffix(this.pending, CLOSE);
|
|
165
|
-
|
|
214
|
+
reasoningDelta2 += this.pending.slice(0, this.pending.length - hold);
|
|
166
215
|
this.pending = this.pending.slice(this.pending.length - hold);
|
|
167
216
|
break;
|
|
168
217
|
}
|
|
169
218
|
this.answer += answerDelta;
|
|
170
|
-
this.reasoning +=
|
|
171
|
-
return { answerDelta, reasoningDelta };
|
|
219
|
+
this.reasoning += reasoningDelta2;
|
|
220
|
+
return { answerDelta, reasoningDelta: reasoningDelta2 };
|
|
172
221
|
}
|
|
173
222
|
finish() {
|
|
174
223
|
if (this.inThink) {
|
|
175
|
-
const
|
|
224
|
+
const reasoningDelta2 = this.pending;
|
|
176
225
|
this.pending = "";
|
|
177
|
-
this.reasoning +=
|
|
178
|
-
return { answerDelta: "", reasoningDelta };
|
|
226
|
+
this.reasoning += reasoningDelta2;
|
|
227
|
+
return { answerDelta: "", reasoningDelta: reasoningDelta2 };
|
|
179
228
|
}
|
|
180
229
|
const answerDelta = this.pending;
|
|
181
230
|
this.pending = "";
|
|
@@ -194,6 +243,11 @@ function splitThinkTags(text) {
|
|
|
194
243
|
function applyResolution(response, prepared) {
|
|
195
244
|
const result = response;
|
|
196
245
|
if (prepared.resolution) result.reasoningResolution = prepared.resolution;
|
|
246
|
+
if (prepared.cacheResolution) result.cacheResolution = prepared.cacheResolution;
|
|
247
|
+
if (prepared.cacheWarnings?.length) {
|
|
248
|
+
result.warnings = [...result.warnings, ...prepared.cacheWarnings];
|
|
249
|
+
}
|
|
250
|
+
applyCacheUsage(result);
|
|
197
251
|
const directive = prepared.directive;
|
|
198
252
|
if (!directive) return result;
|
|
199
253
|
const raw = response.raw?.response;
|
|
@@ -209,6 +263,18 @@ function applyResolution(response, prepared) {
|
|
|
209
263
|
applyUsage(result, raw, directive);
|
|
210
264
|
return result;
|
|
211
265
|
}
|
|
266
|
+
function applyCacheUsage(response) {
|
|
267
|
+
const fromResponse = readCacheTokens(response.raw?.response);
|
|
268
|
+
const fromUsage = readCacheTokens(response.usage?.raw);
|
|
269
|
+
const cachedTokens = fromResponse.cachedTokens ?? fromUsage.cachedTokens;
|
|
270
|
+
const cacheWriteTokens = fromResponse.cacheWriteTokens ?? fromUsage.cacheWriteTokens;
|
|
271
|
+
if (cachedTokens === void 0 && cacheWriteTokens === void 0) return;
|
|
272
|
+
response.usage = {
|
|
273
|
+
...response.usage ?? {},
|
|
274
|
+
...cachedTokens !== void 0 ? { cachedTokens } : {},
|
|
275
|
+
...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
212
278
|
function applyUsage(response, raw, directive) {
|
|
213
279
|
const paths = directive.usage?.reasoningTokenPaths;
|
|
214
280
|
if (!paths || paths.length === 0) return;
|
|
@@ -233,6 +299,9 @@ async function* decorateStream(prepared, source) {
|
|
|
233
299
|
for await (const event of source) {
|
|
234
300
|
if (event.type === "stream.start") {
|
|
235
301
|
yield withResolution(event, prepared);
|
|
302
|
+
for (const warning of prepared.cacheWarnings ?? []) {
|
|
303
|
+
yield { type: "stream.warning", requestId: event.requestId, data: { warning } };
|
|
304
|
+
}
|
|
236
305
|
continue;
|
|
237
306
|
}
|
|
238
307
|
if (event.type === "stream.reasoning.delta") {
|
|
@@ -283,6 +352,7 @@ async function* decorateStream(prepared, source) {
|
|
|
283
352
|
if (splitter) data.text = splitter.answer;
|
|
284
353
|
if (reasoningText) data.reasoningText = reasoningText;
|
|
285
354
|
if (prepared.resolution) data.reasoningResolution = prepared.resolution;
|
|
355
|
+
if (prepared.cacheResolution) data.cacheResolution = prepared.cacheResolution;
|
|
286
356
|
yield { ...event, data };
|
|
287
357
|
continue;
|
|
288
358
|
}
|
|
@@ -290,9 +360,10 @@ async function* decorateStream(prepared, source) {
|
|
|
290
360
|
}
|
|
291
361
|
}
|
|
292
362
|
function withResolution(event, prepared) {
|
|
293
|
-
if (!prepared.resolution) return event;
|
|
363
|
+
if (!prepared.resolution && !prepared.cacheResolution) return event;
|
|
294
364
|
const data = isRecord3(event.data) ? { ...event.data } : {};
|
|
295
|
-
data.reasoningResolution = prepared.resolution;
|
|
365
|
+
if (prepared.resolution) data.reasoningResolution = prepared.resolution;
|
|
366
|
+
if (prepared.cacheResolution) data.cacheResolution = prepared.cacheResolution;
|
|
296
367
|
return { ...event, data };
|
|
297
368
|
}
|
|
298
369
|
function textOf(event) {
|
|
@@ -300,20 +371,37 @@ function textOf(event) {
|
|
|
300
371
|
return event.data.text;
|
|
301
372
|
}
|
|
302
373
|
function adjustUsageEvent(event, prepared) {
|
|
374
|
+
if (!isRecord3(event.data) || !isRecord3(event.data.usage)) return event;
|
|
303
375
|
const paths = prepared.directive?.usage?.reasoningTokenPaths;
|
|
304
|
-
if (!paths || paths.length === 0 || !isRecord3(event.data) || !isRecord3(event.data.usage)) return event;
|
|
305
376
|
const usage = { ...event.data.usage };
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
377
|
+
let changed = false;
|
|
378
|
+
if (paths && paths.length > 0) {
|
|
379
|
+
const tokens = readReasoningTokens(usage.raw, paths);
|
|
380
|
+
if (tokens === void 0) {
|
|
381
|
+
if (usage.reasoningTokens !== void 0) {
|
|
382
|
+
delete usage.reasoningTokens;
|
|
383
|
+
changed = true;
|
|
384
|
+
}
|
|
385
|
+
} else if (usage.reasoningTokens !== tokens) {
|
|
386
|
+
usage.reasoningTokens = tokens;
|
|
387
|
+
changed = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const counts = readCacheTokens(usage.raw);
|
|
391
|
+
if (counts.cachedTokens !== void 0) {
|
|
392
|
+
usage.cachedTokens = counts.cachedTokens;
|
|
393
|
+
changed = true;
|
|
311
394
|
}
|
|
395
|
+
if (counts.cacheWriteTokens !== void 0) {
|
|
396
|
+
usage.cacheWriteTokens = counts.cacheWriteTokens;
|
|
397
|
+
changed = true;
|
|
398
|
+
}
|
|
399
|
+
if (!changed) return event;
|
|
312
400
|
return { ...event, data: { ...event.data, usage } };
|
|
313
401
|
}
|
|
314
402
|
|
|
315
403
|
// src/types/providers.ts
|
|
316
|
-
var IMPLEMENTED_AI_PROVIDERS = ["openrouter", "bedrock", "openai"];
|
|
404
|
+
var IMPLEMENTED_AI_PROVIDERS = ["openrouter", "bedrock", "openai", "cloudflare"];
|
|
317
405
|
function isImplementedAiProvider(provider) {
|
|
318
406
|
return IMPLEMENTED_AI_PROVIDERS.includes(provider);
|
|
319
407
|
}
|
|
@@ -476,105 +564,1775 @@ function resolveAllowlist(filter, tools) {
|
|
|
476
564
|
if (!known.has(name)) {
|
|
477
565
|
throw new AiDispatcherError("MCP_TOOL_NOT_FOUND", `MCP tool "${name}" is not registered.`);
|
|
478
566
|
}
|
|
479
|
-
}
|
|
480
|
-
return new Set(filter);
|
|
567
|
+
}
|
|
568
|
+
return new Set(filter);
|
|
569
|
+
}
|
|
570
|
+
function assertServer(server, seen) {
|
|
571
|
+
if (!isRecord4(server) || typeof server.id !== "string" || server.id.length === 0 || !Array.isArray(server.tools) || typeof server.callTool !== "function") {
|
|
572
|
+
throw new AiDispatcherError(
|
|
573
|
+
"MCP_SERVER_INVALID",
|
|
574
|
+
"An MCP server needs an id, a tools array, and a callTool function."
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (seen.has(server.id)) {
|
|
578
|
+
throw new AiDispatcherError("MCP_SERVER_INVALID", `MCP server "${server.id}" is listed more than once.`);
|
|
579
|
+
}
|
|
580
|
+
seen.add(server.id);
|
|
581
|
+
if (!TOOL_TOKEN.test(server.id)) {
|
|
582
|
+
throw new AiDispatcherError(
|
|
583
|
+
"MCP_TOOL_NAME_INVALID",
|
|
584
|
+
`MCP server id "${server.id}" must use letters, numbers, underscores, and hyphens.`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function exposedToolName(serverId, tool) {
|
|
589
|
+
if (typeof tool.name !== "string" || !TOOL_TOKEN.test(serverId) || !TOOL_TOKEN.test(tool.name)) {
|
|
590
|
+
throw new AiDispatcherError(
|
|
591
|
+
"MCP_TOOL_NAME_INVALID",
|
|
592
|
+
`MCP tool "${serverId}.${typeof tool.name === "string" ? tool.name : ""}" must use letters, numbers, underscores, and hyphens.`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const exposedName = `${serverId}__${tool.name}`;
|
|
596
|
+
if (exposedName.length > MAX_EXPOSED_NAME_LENGTH) {
|
|
597
|
+
throw new AiDispatcherError(
|
|
598
|
+
"MCP_TOOL_NAME_INVALID",
|
|
599
|
+
`MCP tool "${exposedName}" is longer than ${MAX_EXPOSED_NAME_LENGTH} characters.`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
return exposedName;
|
|
603
|
+
}
|
|
604
|
+
function toolDescription(tool) {
|
|
605
|
+
if (tool.description === void 0) return "";
|
|
606
|
+
if (typeof tool.description !== "string") {
|
|
607
|
+
throw new AiDispatcherError(
|
|
608
|
+
"MCP_TOOL_NAME_INVALID",
|
|
609
|
+
`MCP tool "${tool.name}" description must be a string.`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return tool.description;
|
|
613
|
+
}
|
|
614
|
+
function toolParameters(tool) {
|
|
615
|
+
if (tool.inputSchema === void 0) return { ...DEFAULT_PARAMETERS, properties: {} };
|
|
616
|
+
if (!isRecord4(tool.inputSchema)) {
|
|
617
|
+
throw new AiDispatcherError(
|
|
618
|
+
"MCP_TOOL_NAME_INVALID",
|
|
619
|
+
`MCP tool "${tool.name}" inputSchema must be an object.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return tool.inputSchema;
|
|
623
|
+
}
|
|
624
|
+
function textContent(content) {
|
|
625
|
+
if (!Array.isArray(content)) return void 0;
|
|
626
|
+
const parts = [];
|
|
627
|
+
for (const part of content) {
|
|
628
|
+
if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") continue;
|
|
629
|
+
parts.push(part.text);
|
|
630
|
+
}
|
|
631
|
+
if (!parts.length) return void 0;
|
|
632
|
+
return parts.join("\n");
|
|
633
|
+
}
|
|
634
|
+
function toArgs(args) {
|
|
635
|
+
if (isRecord4(args)) return args;
|
|
636
|
+
return {};
|
|
637
|
+
}
|
|
638
|
+
function listFailed(serverId, error) {
|
|
639
|
+
if (error instanceof AiDispatcherError && error.code === "MCP_LIST_FAILED") return error;
|
|
640
|
+
const message = error instanceof Error ? error.message : `MCP server "${serverId}" listTools failed.`;
|
|
641
|
+
return new AiDispatcherError("MCP_LIST_FAILED", message);
|
|
642
|
+
}
|
|
643
|
+
function stringify(result) {
|
|
644
|
+
const serialized = JSON.stringify(result);
|
|
645
|
+
return serialized === void 0 ? "null" : serialized;
|
|
646
|
+
}
|
|
647
|
+
function isRecord4(value) {
|
|
648
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/prepare-request.ts
|
|
652
|
+
import {
|
|
653
|
+
AIProfilesError,
|
|
654
|
+
mergeReasoningBody,
|
|
655
|
+
resolveReasoning
|
|
656
|
+
} from "@x12i/ai-profiles";
|
|
657
|
+
|
|
658
|
+
// src/cache-apply.ts
|
|
659
|
+
var BREAKPOINT = { mode: "explicit" };
|
|
660
|
+
function applyPromptCache(body, plan) {
|
|
661
|
+
if (!plan.stripMarkers && !plan.placeBreakpoint && !plan.sendOpenAiOptions) {
|
|
662
|
+
return { body, placed: false };
|
|
663
|
+
}
|
|
664
|
+
let next = plan.stripMarkers || plan.placeBreakpoint ? stripCacheMarkers(body) : body;
|
|
665
|
+
if (plan.sendOpenAiOptions) next = applyOpenAiOptions(next, plan);
|
|
666
|
+
if (!plan.placeBreakpoint) return { body: next, placed: false };
|
|
667
|
+
if (plan.dialect === "bedrock-cachepoint") return placeBedrockBreakpoint(next, plan.ttl);
|
|
668
|
+
if (plan.dialect === "anthropic-cache-control") return placeAnthropicBreakpoint(next, plan.ttl);
|
|
669
|
+
if (plan.dialect === "openai-responses") return placeOpenAiBreakpoint(next);
|
|
670
|
+
return { body: next, placed: false };
|
|
671
|
+
}
|
|
672
|
+
function applyOpenAiOptions(body, plan) {
|
|
673
|
+
return {
|
|
674
|
+
...body,
|
|
675
|
+
...plan.key ? { prompt_cache_key: plan.key } : {},
|
|
676
|
+
prompt_cache_options: {
|
|
677
|
+
mode: plan.placeBreakpoint ? "explicit" : "implicit",
|
|
678
|
+
...plan.ttl ? { ttl: plan.ttl } : {}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function placeOpenAiBreakpoint(body) {
|
|
683
|
+
const instructions = typeof body.instructions === "string" ? body.instructions.trim() : "";
|
|
684
|
+
if (instructions) {
|
|
685
|
+
const rest = inputItems(body.input);
|
|
686
|
+
return {
|
|
687
|
+
placed: true,
|
|
688
|
+
body: {
|
|
689
|
+
...omitKey(body, "instructions"),
|
|
690
|
+
input: [developerBlock(instructions, "prompt_cache_breakpoint", BREAKPOINT), ...rest]
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (Array.isArray(body.messages)) {
|
|
695
|
+
return markMessages(body, "messages", "prompt_cache_breakpoint", BREAKPOINT, "text");
|
|
696
|
+
}
|
|
697
|
+
return markInput(body, "prompt_cache_breakpoint", BREAKPOINT, "input_text");
|
|
698
|
+
}
|
|
699
|
+
function placeAnthropicBreakpoint(body, ttl) {
|
|
700
|
+
const marker = { type: "ephemeral", ...ttl ? { ttl } : {} };
|
|
701
|
+
if (Array.isArray(body.messages)) {
|
|
702
|
+
const marked = markMessages(body, "messages", "cache_control", marker, "text");
|
|
703
|
+
if (marked.placed) return marked;
|
|
704
|
+
if (typeof body.system === "string" && body.system.trim()) {
|
|
705
|
+
return {
|
|
706
|
+
placed: true,
|
|
707
|
+
body: {
|
|
708
|
+
...body,
|
|
709
|
+
system: [{ type: "text", text: body.system.trim(), cache_control: marker }]
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return marked;
|
|
714
|
+
}
|
|
715
|
+
const instructions = typeof body.instructions === "string" ? body.instructions.trim() : "";
|
|
716
|
+
if (instructions) {
|
|
717
|
+
const rest = inputItems(body.input);
|
|
718
|
+
return {
|
|
719
|
+
placed: true,
|
|
720
|
+
body: {
|
|
721
|
+
...omitKey(body, "instructions"),
|
|
722
|
+
input: [developerBlock(instructions, "cache_control", marker), ...rest]
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return markInput(body, "cache_control", marker, "input_text");
|
|
727
|
+
}
|
|
728
|
+
function placeBedrockBreakpoint(body, ttl) {
|
|
729
|
+
const point = { cachePoint: { type: "default", ...ttl ? { ttl } : {} } };
|
|
730
|
+
const system = Array.isArray(body.system) ? body.system.filter(hasBedrockText) : [];
|
|
731
|
+
if (system.length > 0) {
|
|
732
|
+
return { placed: true, body: { ...body, system: [...system, point] } };
|
|
733
|
+
}
|
|
734
|
+
return { body, placed: false };
|
|
735
|
+
}
|
|
736
|
+
function markMessages(body, field, markerKey, markerValue, textType) {
|
|
737
|
+
const messages = body[field];
|
|
738
|
+
if (!Array.isArray(messages)) return { body, placed: false };
|
|
739
|
+
const index = stableMessageIndex(messages);
|
|
740
|
+
if (index < 0) return { body, placed: false };
|
|
741
|
+
const marked = markMessage(messages[index], markerKey, markerValue, textType);
|
|
742
|
+
if (!marked) return { body, placed: false };
|
|
743
|
+
const next = messages.slice();
|
|
744
|
+
next[index] = marked;
|
|
745
|
+
return { placed: true, body: { ...body, [field]: next } };
|
|
746
|
+
}
|
|
747
|
+
function markInput(body, markerKey, markerValue, textType) {
|
|
748
|
+
if (!Array.isArray(body.input)) return { body, placed: false };
|
|
749
|
+
const index = stableInputIndex(body.input);
|
|
750
|
+
if (index < 0) return { body, placed: false };
|
|
751
|
+
const marked = markMessage(body.input[index], markerKey, markerValue, textType);
|
|
752
|
+
if (!marked) return { body, placed: false };
|
|
753
|
+
const next = body.input.slice();
|
|
754
|
+
next[index] = marked;
|
|
755
|
+
return { placed: true, body: { ...body, input: next } };
|
|
756
|
+
}
|
|
757
|
+
function stableMessageIndex(messages) {
|
|
758
|
+
let lastSystem = -1;
|
|
759
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
760
|
+
const message = messages[index];
|
|
761
|
+
if (isPlainRecord(message) && message.role === "system" && hasText(message.content)) lastSystem = index;
|
|
762
|
+
}
|
|
763
|
+
return lastSystem;
|
|
764
|
+
}
|
|
765
|
+
function stableInputIndex(items) {
|
|
766
|
+
let lastStable = -1;
|
|
767
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
768
|
+
const item = items[index];
|
|
769
|
+
if (!isPlainRecord(item) || !hasText(item.content)) continue;
|
|
770
|
+
if (item.role === "system" || item.role === "developer") lastStable = index;
|
|
771
|
+
}
|
|
772
|
+
return lastStable;
|
|
773
|
+
}
|
|
774
|
+
function markMessage(message, markerKey, markerValue, textType) {
|
|
775
|
+
if (!isPlainRecord(message) || !hasText(message.content)) return void 0;
|
|
776
|
+
const content = markContent(message.content, markerKey, markerValue, textType);
|
|
777
|
+
if (!content) return void 0;
|
|
778
|
+
return { ...message, content };
|
|
779
|
+
}
|
|
780
|
+
function markContent(content, markerKey, markerValue, textType) {
|
|
781
|
+
if (typeof content === "string") {
|
|
782
|
+
if (!content.trim()) return void 0;
|
|
783
|
+
return [{ type: textType, text: content, [markerKey]: markerValue }];
|
|
784
|
+
}
|
|
785
|
+
if (!Array.isArray(content)) return void 0;
|
|
786
|
+
const parts = content.map((part) => isPlainRecord(part) ? { ...part } : part);
|
|
787
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
788
|
+
const part = parts[index];
|
|
789
|
+
if (!isPlainRecord(part) || typeof part.text !== "string" || !part.text.trim()) continue;
|
|
790
|
+
parts[index] = { ...part, [markerKey]: markerValue };
|
|
791
|
+
return parts;
|
|
792
|
+
}
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
function developerBlock(text, markerKey, markerValue) {
|
|
796
|
+
return {
|
|
797
|
+
role: "developer",
|
|
798
|
+
content: [{ type: "input_text", text, [markerKey]: markerValue }]
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function inputItems(input) {
|
|
802
|
+
if (input === void 0) return [];
|
|
803
|
+
if (typeof input === "string") {
|
|
804
|
+
return input.trim() ? [{ role: "user", content: input }] : [];
|
|
805
|
+
}
|
|
806
|
+
return Array.isArray(input) ? input : [];
|
|
807
|
+
}
|
|
808
|
+
function hasBedrockText(block) {
|
|
809
|
+
return isPlainRecord(block) && typeof block.text === "string" && block.text.trim().length > 0;
|
|
810
|
+
}
|
|
811
|
+
function hasText(content) {
|
|
812
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
813
|
+
if (!Array.isArray(content)) return false;
|
|
814
|
+
return content.some((part) => isPlainRecord(part) && typeof part.text === "string" && part.text.trim().length > 0);
|
|
815
|
+
}
|
|
816
|
+
function stripCacheMarkers(value) {
|
|
817
|
+
return stripValue(value);
|
|
818
|
+
}
|
|
819
|
+
function stripValue(value) {
|
|
820
|
+
if (Array.isArray(value)) {
|
|
821
|
+
return value.map((item) => stripValue(item)).filter((item) => !(isPlainRecord(item) && Object.keys(item).length === 0));
|
|
822
|
+
}
|
|
823
|
+
if (!isPlainRecord(value)) return value;
|
|
824
|
+
const next = {};
|
|
825
|
+
for (const [key, child] of Object.entries(value)) {
|
|
826
|
+
if (key === "prompt_cache_breakpoint" || key === "cache_control" || key === "cachePoint") continue;
|
|
827
|
+
if (key === "prompt_cache_key" || key === "prompt_cache_options") continue;
|
|
828
|
+
next[key] = stripValue(child);
|
|
829
|
+
}
|
|
830
|
+
return next;
|
|
831
|
+
}
|
|
832
|
+
function omitKey(body, key) {
|
|
833
|
+
const next = { ...body };
|
|
834
|
+
delete next[key];
|
|
835
|
+
return next;
|
|
836
|
+
}
|
|
837
|
+
function isPlainRecord(value) {
|
|
838
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
839
|
+
const proto = Object.getPrototypeOf(value);
|
|
840
|
+
return proto === Object.prototype || proto === null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/cache-profiles.ts
|
|
844
|
+
var TTL_MINUTES = { "5m": 5, "30m": 30, "1h": 60 };
|
|
845
|
+
var TTL_SECONDS = { "5m": 300, "30m": 1800, "1h": 3600 };
|
|
846
|
+
var OPENAI_EXPLICIT = {
|
|
847
|
+
dialect: "openai-responses",
|
|
848
|
+
ttlOptions: ["30m"],
|
|
849
|
+
defaultTtl: "30m"
|
|
850
|
+
};
|
|
851
|
+
var OPENAI_IMPLICIT = {
|
|
852
|
+
dialect: "implicit-native",
|
|
853
|
+
ttlOptions: ["30m"],
|
|
854
|
+
defaultTtl: "30m"
|
|
855
|
+
};
|
|
856
|
+
var ANTHROPIC_EXPLICIT = {
|
|
857
|
+
dialect: "anthropic-cache-control",
|
|
858
|
+
ttlOptions: ["5m", "1h"],
|
|
859
|
+
defaultTtl: "5m"
|
|
860
|
+
};
|
|
861
|
+
var QWEN_EXPLICIT = {
|
|
862
|
+
dialect: "anthropic-cache-control",
|
|
863
|
+
ttlOptions: ["5m"],
|
|
864
|
+
defaultTtl: "5m"
|
|
865
|
+
};
|
|
866
|
+
var BEDROCK_EXPLICIT = {
|
|
867
|
+
dialect: "bedrock-cachepoint",
|
|
868
|
+
ttlOptions: ["5m", "1h"],
|
|
869
|
+
defaultTtl: "5m"
|
|
870
|
+
};
|
|
871
|
+
var ROUTER_IMPLICIT = {
|
|
872
|
+
dialect: "implicit-native",
|
|
873
|
+
ttlOptions: ["5m"],
|
|
874
|
+
defaultTtl: "5m"
|
|
875
|
+
};
|
|
876
|
+
function resolvePromptCache(input) {
|
|
877
|
+
const mode = normalizeMode(input.policy.mode);
|
|
878
|
+
const key = input.policy.key?.trim() || void 0;
|
|
879
|
+
const requestedTtl = normalizeTtl(input.policy.ttl);
|
|
880
|
+
const model = input.effectiveModel.trim();
|
|
881
|
+
const resolutionBase = {
|
|
882
|
+
mode,
|
|
883
|
+
target: input.target,
|
|
884
|
+
requestedModel: input.requestedModel,
|
|
885
|
+
effectiveModel: input.effectiveModel,
|
|
886
|
+
keyPresent: Boolean(key)
|
|
887
|
+
};
|
|
888
|
+
if (input.target === "cloudflare") {
|
|
889
|
+
return resolveCloudflarePromptCache({ input, mode, key, requestedTtl, resolutionBase, model });
|
|
890
|
+
}
|
|
891
|
+
if (mode === "disabled") {
|
|
892
|
+
return {
|
|
893
|
+
plan: emptyPlan(mode, "none", key, true),
|
|
894
|
+
resolution: { ...resolutionBase, outcome: "ignored" },
|
|
895
|
+
warnings: []
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (input.policy.mode !== void 0 && mode === "explicit" && !isCacheMode(input.policy.mode)) {
|
|
899
|
+
return ignored(input, resolutionBase, key, "CACHE_UNSUPPORTED", "cache.mode is not auto, explicit, or disabled. The call still runs without cache markers.");
|
|
900
|
+
}
|
|
901
|
+
const matched = matchProfile(input.target, model);
|
|
902
|
+
if (!matched.profile) {
|
|
903
|
+
return ignored(
|
|
904
|
+
input,
|
|
905
|
+
resolutionBase,
|
|
906
|
+
key,
|
|
907
|
+
matched.code,
|
|
908
|
+
matched.message
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
return applyProfile({ input, mode, key, requestedTtl, resolutionBase, model, profile: matched.profile });
|
|
912
|
+
}
|
|
913
|
+
function resolveCloudflarePromptCache(args) {
|
|
914
|
+
const { input, mode, key, requestedTtl, resolutionBase, model } = args;
|
|
915
|
+
if (mode === "disabled") {
|
|
916
|
+
return {
|
|
917
|
+
plan: { ...emptyPlan(mode, "none", key, true), gateway: gatewayDirective(true, void 0, key) },
|
|
918
|
+
resolution: { ...resolutionBase, outcome: "applied" },
|
|
919
|
+
warnings: []
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (input.policy.mode !== void 0 && mode === "explicit" && !isCacheMode(input.policy.mode)) {
|
|
923
|
+
return ignored(input, resolutionBase, key, "CACHE_UNSUPPORTED", "cache.mode is not auto, explicit, or disabled. The call still runs without cache markers.");
|
|
924
|
+
}
|
|
925
|
+
const profile = matchCloudflareNative(input.cloudflareEndpoint, model);
|
|
926
|
+
if (!profile) {
|
|
927
|
+
return {
|
|
928
|
+
plan: {
|
|
929
|
+
mode,
|
|
930
|
+
dialect: "none",
|
|
931
|
+
placeBreakpoint: false,
|
|
932
|
+
sendOpenAiOptions: false,
|
|
933
|
+
stripMarkers: false,
|
|
934
|
+
gateway: gatewayDirective(false, requestedTtl, key),
|
|
935
|
+
...requestedTtl ? { ttl: requestedTtl } : {},
|
|
936
|
+
...key ? { key } : {}
|
|
937
|
+
},
|
|
938
|
+
resolution: {
|
|
939
|
+
...resolutionBase,
|
|
940
|
+
outcome: "applied",
|
|
941
|
+
...requestedTtl ? { ttl: requestedTtl } : {}
|
|
942
|
+
},
|
|
943
|
+
warnings: []
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const resolved = applyProfile({ input, mode, key, requestedTtl, resolutionBase, model, profile });
|
|
947
|
+
const ttl = resolved.plan.ttl ?? requestedTtl;
|
|
948
|
+
return {
|
|
949
|
+
...resolved,
|
|
950
|
+
plan: { ...resolved.plan, gateway: gatewayDirective(false, ttl, key) }
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function applyProfile(args) {
|
|
954
|
+
const { input, mode, key, requestedTtl, resolutionBase, model, profile } = args;
|
|
955
|
+
const folded = foldTtl(requestedTtl, profile.ttlOptions, profile.defaultTtl);
|
|
956
|
+
const explicitDialect = profile.dialect === "openai-responses" || profile.dialect === "anthropic-cache-control" || profile.dialect === "bedrock-cachepoint";
|
|
957
|
+
if (mode === "explicit" && profile.dialect === "implicit-native") {
|
|
958
|
+
return ignored(
|
|
959
|
+
input,
|
|
960
|
+
resolutionBase,
|
|
961
|
+
key,
|
|
962
|
+
"CACHE_UNSUPPORTED",
|
|
963
|
+
"This model caches automatically and does not accept an explicit breakpoint. The call still runs."
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (mode === "auto" && explicitDialect && profile.dialect !== "openai-responses") {
|
|
967
|
+
return ignored(
|
|
968
|
+
input,
|
|
969
|
+
resolutionBase,
|
|
970
|
+
key,
|
|
971
|
+
"CACHE_UNSUPPORTED",
|
|
972
|
+
"This model only caches when an explicit breakpoint is set. Set cache.mode to explicit. The call still runs."
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const warnings = [];
|
|
976
|
+
const sendsTtl = profile.dialect === "openai-responses" || mode === "explicit" && explicitDialect;
|
|
977
|
+
if (requestedTtl && folded.degraded && sendsTtl) {
|
|
978
|
+
warnings.push({
|
|
979
|
+
code: "CACHE_TTL_DEGRADED",
|
|
980
|
+
message: `cache.ttl "${requestedTtl}" is not supported for this model. Using "${folded.ttl}".`,
|
|
981
|
+
details: { requestedTtl, effectiveTtl: folded.ttl, target: input.target, model }
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const placeBreakpoint = mode === "explicit" && explicitDialect;
|
|
985
|
+
const sendOpenAiOptions = profile.dialect === "openai-responses" && (mode === "explicit" || mode === "auto");
|
|
986
|
+
const outcome = folded.degraded && sendsTtl ? "degraded" : "applied";
|
|
987
|
+
return {
|
|
988
|
+
plan: {
|
|
989
|
+
mode,
|
|
990
|
+
dialect: profile.dialect,
|
|
991
|
+
placeBreakpoint,
|
|
992
|
+
sendOpenAiOptions,
|
|
993
|
+
stripMarkers: placeBreakpoint,
|
|
994
|
+
...sendsTtl ? { ttl: folded.ttl } : {},
|
|
995
|
+
...key ? { key } : {}
|
|
996
|
+
},
|
|
997
|
+
resolution: {
|
|
998
|
+
...resolutionBase,
|
|
999
|
+
outcome,
|
|
1000
|
+
...sendsTtl ? { ttl: folded.ttl } : {}
|
|
1001
|
+
},
|
|
1002
|
+
warnings
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function gatewayDirective(skip, ttl, key) {
|
|
1006
|
+
return {
|
|
1007
|
+
skip,
|
|
1008
|
+
...ttl ? { ttlSeconds: TTL_SECONDS[ttl] } : {},
|
|
1009
|
+
...key ? { key } : {}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function matchCloudflareNative(endpoint, model) {
|
|
1013
|
+
const id = model.toLowerCase();
|
|
1014
|
+
if (endpoint === "responses" && isGptExplicit(id)) return OPENAI_EXPLICIT;
|
|
1015
|
+
if (endpoint === "responses" && isGptImplicit(id)) return OPENAI_IMPLICIT;
|
|
1016
|
+
if (endpoint === "messages" && isClaude(id)) return ANTHROPIC_EXPLICIT;
|
|
1017
|
+
return void 0;
|
|
1018
|
+
}
|
|
1019
|
+
function ignored(input, resolutionBase, key, code, message) {
|
|
1020
|
+
return {
|
|
1021
|
+
plan: emptyPlan(resolutionBase.mode, "none", key, false),
|
|
1022
|
+
resolution: { ...resolutionBase, outcome: "ignored" },
|
|
1023
|
+
warnings: [{
|
|
1024
|
+
code,
|
|
1025
|
+
message,
|
|
1026
|
+
details: { target: input.target, model: input.effectiveModel.trim() }
|
|
1027
|
+
}]
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function emptyPlan(mode, dialect, key, stripMarkers) {
|
|
1031
|
+
return {
|
|
1032
|
+
mode,
|
|
1033
|
+
dialect,
|
|
1034
|
+
placeBreakpoint: false,
|
|
1035
|
+
sendOpenAiOptions: false,
|
|
1036
|
+
stripMarkers,
|
|
1037
|
+
...key ? { key } : {}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function matchProfile(target, model) {
|
|
1041
|
+
const id = model.toLowerCase();
|
|
1042
|
+
if (!id) {
|
|
1043
|
+
return {
|
|
1044
|
+
code: "CACHE_UNSUPPORTED",
|
|
1045
|
+
message: "Prompt caching needs a model id. The call still runs without cache markers."
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
if (target === "bedrock") {
|
|
1049
|
+
if (isOpenAiBedrockId(id)) {
|
|
1050
|
+
return {
|
|
1051
|
+
code: "CACHE_DIALECT_UNSUPPORTED",
|
|
1052
|
+
message: "OpenAI models on Bedrock cache through the Responses API. This runtime sends Converse and does not add those markers. The call still runs."
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
if (isClaude(id) || isNova(id)) return { profile: BEDROCK_EXPLICIT, code: "", message: "" };
|
|
1056
|
+
return {
|
|
1057
|
+
code: "CACHE_UNSUPPORTED",
|
|
1058
|
+
message: "This Bedrock model has no prompt-cache profile. The call still runs without cache markers."
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
if (target === "openai") {
|
|
1062
|
+
if (isGptExplicit(id)) return { profile: OPENAI_EXPLICIT, code: "", message: "" };
|
|
1063
|
+
if (isGptImplicit(id)) return { profile: OPENAI_IMPLICIT, code: "", message: "" };
|
|
1064
|
+
return {
|
|
1065
|
+
code: "CACHE_UNSUPPORTED",
|
|
1066
|
+
message: "This OpenAI model has no prompt-cache profile. The call still runs without cache markers."
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (isClaude(id)) return { profile: ANTHROPIC_EXPLICIT, code: "", message: "" };
|
|
1070
|
+
if (isQwen(id)) return { profile: QWEN_EXPLICIT, code: "", message: "" };
|
|
1071
|
+
if (isGptExplicit(id)) return { profile: OPENAI_EXPLICIT, code: "", message: "" };
|
|
1072
|
+
if (isGptImplicit(id) || isRouterImplicit(id)) return { profile: ROUTER_IMPLICIT, code: "", message: "" };
|
|
1073
|
+
return {
|
|
1074
|
+
code: "CACHE_UNSUPPORTED",
|
|
1075
|
+
message: "This model has no prompt-cache profile. The call still runs without cache markers."
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function foldTtl(requested, options, defaultTtl) {
|
|
1079
|
+
if (!requested) return { ttl: defaultTtl, degraded: false };
|
|
1080
|
+
if (options.includes(requested)) return { ttl: requested, degraded: false };
|
|
1081
|
+
let best = defaultTtl;
|
|
1082
|
+
let bestDistance = Math.abs(TTL_MINUTES[defaultTtl] - TTL_MINUTES[requested]);
|
|
1083
|
+
for (const option of options) {
|
|
1084
|
+
const distance = Math.abs(TTL_MINUTES[option] - TTL_MINUTES[requested]);
|
|
1085
|
+
if (distance < bestDistance) {
|
|
1086
|
+
best = option;
|
|
1087
|
+
bestDistance = distance;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return { ttl: best, degraded: true };
|
|
1091
|
+
}
|
|
1092
|
+
function normalizeMode(mode) {
|
|
1093
|
+
if (mode === "auto" || mode === "disabled") return mode;
|
|
1094
|
+
return "explicit";
|
|
1095
|
+
}
|
|
1096
|
+
function isCacheMode(mode) {
|
|
1097
|
+
return mode === "auto" || mode === "explicit" || mode === "disabled";
|
|
1098
|
+
}
|
|
1099
|
+
function normalizeTtl(ttl) {
|
|
1100
|
+
if (ttl === "5m" || ttl === "30m" || ttl === "1h") return ttl;
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
function isGptExplicit(id) {
|
|
1104
|
+
return /(?:^|\/)gpt-(?:5\.6|6)(?:[.\-:]|$)/i.test(id);
|
|
1105
|
+
}
|
|
1106
|
+
function isGptImplicit(id) {
|
|
1107
|
+
if (isGptExplicit(id)) return false;
|
|
1108
|
+
return /(?:^|\/)gpt(?:[.\-:]|$)/i.test(id) || /(?:^|\/)o[134](?:[.\-:]|$)/i.test(id);
|
|
1109
|
+
}
|
|
1110
|
+
function isClaude(id) {
|
|
1111
|
+
return id.includes("claude") || id.startsWith("anthropic.") || id.startsWith("anthropic/");
|
|
1112
|
+
}
|
|
1113
|
+
function isQwen(id) {
|
|
1114
|
+
return id.includes("qwen");
|
|
1115
|
+
}
|
|
1116
|
+
function isNova(id) {
|
|
1117
|
+
return /(?:^|[./])nova(?:[.\-:]|$)/i.test(id) || id.includes("amazon.nova");
|
|
1118
|
+
}
|
|
1119
|
+
function isOpenAiBedrockId(id) {
|
|
1120
|
+
return id.startsWith("openai.") || id.startsWith("openai/") || isGptExplicit(id) || isGptImplicit(id);
|
|
1121
|
+
}
|
|
1122
|
+
function isRouterImplicit(id) {
|
|
1123
|
+
return /(?:^|\/)(gemini|google|deepseek|x-ai|xai|grok|moonshot|kimi)(?:[./:\-]|$)/i.test(id) || id.includes("gemini") || id.includes("deepseek");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/providers/cloudflare.ts
|
|
1127
|
+
import {
|
|
1128
|
+
OPENROUTER_METADATA_LIMITS,
|
|
1129
|
+
toProviderMetadata
|
|
1130
|
+
} from "@x12i/openrouter-runtime";
|
|
1131
|
+
var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
1132
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
1133
|
+
var DEFAULT_GATEWAY_ID = "default";
|
|
1134
|
+
function selectCloudflareEndpoint(input) {
|
|
1135
|
+
if (input.endpoint && input.endpoint !== "auto") return input.endpoint;
|
|
1136
|
+
if (input.apiMode === "chat") return "chat";
|
|
1137
|
+
if (input.apiMode === "responses") return "responses";
|
|
1138
|
+
const id = input.model.trim().toLowerCase();
|
|
1139
|
+
if (id.startsWith("anthropic/")) return "messages";
|
|
1140
|
+
if (id.startsWith("openai/") || isBareOpenAiModel(id)) return "responses";
|
|
1141
|
+
return "chat";
|
|
1142
|
+
}
|
|
1143
|
+
function cloudflareCatalogQuery(model) {
|
|
1144
|
+
const id = model.trim();
|
|
1145
|
+
const lower = id.toLowerCase();
|
|
1146
|
+
if (lower.startsWith("openai/")) {
|
|
1147
|
+
const slash = id.indexOf("/");
|
|
1148
|
+
return { target: "openai", model: id.slice(slash + 1), prefix: id.slice(0, slash + 1) };
|
|
1149
|
+
}
|
|
1150
|
+
if (isBareOpenAiModel(id)) return { target: "openai", model: id, prefix: "openai/" };
|
|
1151
|
+
return { target: "openrouter", model: id };
|
|
1152
|
+
}
|
|
1153
|
+
function restoreCloudflareModel(model, prefix) {
|
|
1154
|
+
if (!prefix || model.includes("/")) return model;
|
|
1155
|
+
return `${prefix}${model}`;
|
|
1156
|
+
}
|
|
1157
|
+
function createCloudflareProviderAdapter(options = {}) {
|
|
1158
|
+
const resolved = resolveCloudflareOptions(options);
|
|
1159
|
+
return {
|
|
1160
|
+
compile(request, call) {
|
|
1161
|
+
return compileCloudflareRequest(request, resolved, call, false);
|
|
1162
|
+
},
|
|
1163
|
+
async run(request, call) {
|
|
1164
|
+
const requestId = request.id ?? "cloudflare";
|
|
1165
|
+
try {
|
|
1166
|
+
return await runCloudflare(request, resolved, call, requestId);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (error instanceof AiDispatcherError && isConfigError(error.code)) throw error;
|
|
1169
|
+
return failedResponse(request, requestId, error, "chat");
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
executeStreamingChat(request, call) {
|
|
1173
|
+
return streamCloudflare(request, resolved, call);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
function compileCloudflareRequest(request, options, call, streaming) {
|
|
1178
|
+
assertSupported(request);
|
|
1179
|
+
assertCredentials(options);
|
|
1180
|
+
const model = (request.model ?? options.defaultModel ?? "").trim();
|
|
1181
|
+
if (!model) {
|
|
1182
|
+
throw new AiDispatcherError(
|
|
1183
|
+
"MODEL_REQUIRED",
|
|
1184
|
+
"A model id is required. Set request.model or cloudflare.defaultModel."
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const endpoint = call?.cloudflare?.endpoint ?? selectCloudflareEndpoint({
|
|
1188
|
+
model,
|
|
1189
|
+
...options.endpoint ? { endpoint: options.endpoint } : {},
|
|
1190
|
+
...request.apiMode ? { apiMode: request.apiMode } : {}
|
|
1191
|
+
});
|
|
1192
|
+
if (streaming && endpoint === "run") {
|
|
1193
|
+
throw new AiDispatcherError(
|
|
1194
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1195
|
+
"Cloudflare /ai/run does not stream. Use chat, responses, or messages, or call run()."
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
if ((endpoint === "messages" || endpoint === "run") && request.responseFormat !== void 0) {
|
|
1199
|
+
throw new AiDispatcherError(
|
|
1200
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1201
|
+
"responseFormat is supported on Cloudflare chat and responses, not on messages or run."
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const run = endpoint === "run" ? call?.cloudflare?.run : void 0;
|
|
1205
|
+
assertRunOptions(run);
|
|
1206
|
+
let body = endpoint === "responses" ? compileResponsesBody(request, model) : endpoint === "messages" ? compileMessagesBody(request, model) : compileChatBody(request, model);
|
|
1207
|
+
if (call?.finalizeBody) body = call.finalizeBody(body);
|
|
1208
|
+
if (endpoint !== "run") body = { ...body, stream: streaming };
|
|
1209
|
+
const gateway = mergeGateway(options.gateway, call?.cloudflare?.gateway);
|
|
1210
|
+
const fromBody = isRecord5(body.metadata) ? body.metadata : void 0;
|
|
1211
|
+
const record2 = toProviderMetadata(
|
|
1212
|
+
request.metadata,
|
|
1213
|
+
request.agentId,
|
|
1214
|
+
OPENROUTER_METADATA_LIMITS,
|
|
1215
|
+
overlayMetadata(gateway?.metadata, fromBody)
|
|
1216
|
+
);
|
|
1217
|
+
body = applyProviderRecord(body, record2);
|
|
1218
|
+
if (endpoint === "run") body = wrapRun(body, run);
|
|
1219
|
+
return {
|
|
1220
|
+
endpoint,
|
|
1221
|
+
url: endpointUrl(options, endpoint),
|
|
1222
|
+
headers: buildHeaders(options, call?.cloudflare, model, record2),
|
|
1223
|
+
body,
|
|
1224
|
+
warnings: []
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function compileResponsesBody(request, model) {
|
|
1228
|
+
const instructions = compileInstructions(request);
|
|
1229
|
+
const input = compileResponsesInput(request);
|
|
1230
|
+
if (input === void 0) {
|
|
1231
|
+
throw new AiDispatcherError("INPUT_REQUIRED", "Provide request.messages, request.input, or request.prompt.");
|
|
1232
|
+
}
|
|
1233
|
+
return clean({
|
|
1234
|
+
model,
|
|
1235
|
+
input,
|
|
1236
|
+
instructions,
|
|
1237
|
+
temperature: request.temperature,
|
|
1238
|
+
max_output_tokens: request.maxTokens,
|
|
1239
|
+
reasoning: request.reasoning,
|
|
1240
|
+
text: request.responseFormat,
|
|
1241
|
+
tools: compileResponsesTools(request),
|
|
1242
|
+
tool_choice: compileResponsesToolChoice(request)
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function compileChatBody(request, model) {
|
|
1246
|
+
const messages = compileChatMessages(request);
|
|
1247
|
+
if (!messages.length) {
|
|
1248
|
+
throw new AiDispatcherError("INPUT_REQUIRED", "Provide request.messages, request.input, or request.prompt.");
|
|
1249
|
+
}
|
|
1250
|
+
return clean({
|
|
1251
|
+
model,
|
|
1252
|
+
messages,
|
|
1253
|
+
temperature: request.temperature,
|
|
1254
|
+
max_tokens: request.maxTokens,
|
|
1255
|
+
reasoning: request.reasoning,
|
|
1256
|
+
response_format: request.responseFormat,
|
|
1257
|
+
tools: compileChatTools(request),
|
|
1258
|
+
tool_choice: compileChatToolChoice(request)
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function compileMessagesBody(request, model) {
|
|
1262
|
+
const system = compileInstructions(request);
|
|
1263
|
+
const messages = compileAnthropicMessages(request);
|
|
1264
|
+
if (!messages.length) {
|
|
1265
|
+
throw new AiDispatcherError("INPUT_REQUIRED", "Provide request.messages, request.input, or request.prompt.");
|
|
1266
|
+
}
|
|
1267
|
+
return clean({
|
|
1268
|
+
model,
|
|
1269
|
+
system,
|
|
1270
|
+
messages,
|
|
1271
|
+
temperature: request.temperature,
|
|
1272
|
+
max_tokens: request.maxTokens,
|
|
1273
|
+
tools: compileAnthropicTools(request),
|
|
1274
|
+
tool_choice: compileAnthropicToolChoice(request)
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
function wrapRun(body, run) {
|
|
1278
|
+
const model = body.model;
|
|
1279
|
+
const input = { ...body };
|
|
1280
|
+
delete input.model;
|
|
1281
|
+
delete input.stream;
|
|
1282
|
+
return clean({
|
|
1283
|
+
model,
|
|
1284
|
+
input,
|
|
1285
|
+
options: runOptions(run)
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
function runOptions(run) {
|
|
1289
|
+
if (!run) return void 0;
|
|
1290
|
+
return clean({
|
|
1291
|
+
background: run.background,
|
|
1292
|
+
webhookUrl: run.webhookUrl,
|
|
1293
|
+
webhookFormat: run.webhookFormat
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
function assertRunOptions(run) {
|
|
1297
|
+
if (!run) return;
|
|
1298
|
+
if (run.webhookUrl && run.background !== true) {
|
|
1299
|
+
throw new AiDispatcherError(
|
|
1300
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1301
|
+
"Cloudflare webhookUrl requires run.background to be true."
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
if (run.webhookFormat && !run.webhookUrl) {
|
|
1305
|
+
throw new AiDispatcherError(
|
|
1306
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1307
|
+
"Cloudflare webhookFormat requires run.webhookUrl."
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function assertSupported(request) {
|
|
1312
|
+
if (request.serverTools && serverToolsEnabled(request.serverTools)) {
|
|
1313
|
+
throw new AiDispatcherError(
|
|
1314
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1315
|
+
"Cloudflare does not support OpenRouter server tools."
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
if (request.rawOpenRouterOverrides !== void 0) {
|
|
1319
|
+
throw new AiDispatcherError(
|
|
1320
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1321
|
+
"rawOpenRouterOverrides cannot be sent to Cloudflare."
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
if (request.toolChoice && typeof request.toolChoice === "object" && request.toolChoice.type === "server_tool") {
|
|
1325
|
+
throw new AiDispatcherError(
|
|
1326
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1327
|
+
"Cloudflare does not support OpenRouter server tool choices."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function assertCredentials(options) {
|
|
1332
|
+
if (!options.apiToken) {
|
|
1333
|
+
throw new AiDispatcherError(
|
|
1334
|
+
"CLOUDFLARE_API_TOKEN_MISSING",
|
|
1335
|
+
"Cloudflare API token is required. Set cloudflare.apiToken or CLOUDFLARE_API_TOKEN."
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (!options.accountId) {
|
|
1339
|
+
throw new AiDispatcherError(
|
|
1340
|
+
"CLOUDFLARE_ACCOUNT_ID_MISSING",
|
|
1341
|
+
"Cloudflare account id is required. Set cloudflare.accountId or CLOUDFLARE_ACCOUNT_ID."
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function serverToolsEnabled(policy) {
|
|
1346
|
+
return Object.values(policy).some((value) => {
|
|
1347
|
+
if (Array.isArray(value)) return value.some((item) => isEnabledMode(item));
|
|
1348
|
+
return isEnabledMode(value);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
function isEnabledMode(value) {
|
|
1352
|
+
return typeof value === "object" && value !== null && "mode" in value && value.mode !== "disabled";
|
|
1353
|
+
}
|
|
1354
|
+
function compileInstructions(request) {
|
|
1355
|
+
if (request.instructions) return request.instructions;
|
|
1356
|
+
if (request.system) return request.system;
|
|
1357
|
+
const systemMessages = (request.messages ?? []).filter((message) => message.role === "system");
|
|
1358
|
+
if (!systemMessages.length) return void 0;
|
|
1359
|
+
return systemMessages.map((message) => contentToText(message.content)).filter(Boolean).join("\n");
|
|
1360
|
+
}
|
|
1361
|
+
function compileResponsesInput(request) {
|
|
1362
|
+
if (request.input !== void 0) return request.input;
|
|
1363
|
+
const items = [];
|
|
1364
|
+
for (const message of request.messages ?? []) {
|
|
1365
|
+
if (message.role === "system") continue;
|
|
1366
|
+
items.push(compileResponsesItem(message));
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof request.prompt === "string") items.push({ role: "user", content: request.prompt });
|
|
1369
|
+
return items.length ? items : void 0;
|
|
1370
|
+
}
|
|
1371
|
+
function compileResponsesItem(message) {
|
|
1372
|
+
if (message.role === "tool") {
|
|
1373
|
+
return {
|
|
1374
|
+
type: "function_call_output",
|
|
1375
|
+
call_id: message.toolCallId ?? message.name ?? "tool",
|
|
1376
|
+
output: contentToText(message.content)
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
1380
|
+
return { role, content: compileResponsesContent(message.content) };
|
|
1381
|
+
}
|
|
1382
|
+
function compileResponsesContent(content) {
|
|
1383
|
+
if (typeof content === "string") return content;
|
|
1384
|
+
return content.map((part) => {
|
|
1385
|
+
if (part.type === "text") return { type: "input_text", text: part.text };
|
|
1386
|
+
if (part.type === "image_url") return { type: "input_image", image_url: part.imageUrl };
|
|
1387
|
+
return compileFilePart(part, "responses");
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
function compileChatMessages(request) {
|
|
1391
|
+
assertExclusiveInput(request);
|
|
1392
|
+
const messages = [];
|
|
1393
|
+
const system = compileInstructions(request);
|
|
1394
|
+
if (system) messages.push({ role: "system", content: system });
|
|
1395
|
+
for (const message of dialogueMessages(request)) messages.push(compileChatMessage(message));
|
|
1396
|
+
return messages;
|
|
1397
|
+
}
|
|
1398
|
+
function compileChatMessage(message) {
|
|
1399
|
+
if (message.role === "tool") {
|
|
1400
|
+
return {
|
|
1401
|
+
role: "tool",
|
|
1402
|
+
tool_call_id: message.toolCallId ?? message.name ?? "tool",
|
|
1403
|
+
content: contentToText(message.content)
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
1408
|
+
content: compileChatContent(message.content)
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function compileChatContent(content) {
|
|
1412
|
+
if (typeof content === "string") return content;
|
|
1413
|
+
return content.map((part) => {
|
|
1414
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
1415
|
+
if (part.type === "image_url") return { type: "image_url", image_url: { url: part.imageUrl } };
|
|
1416
|
+
const file = compileFilePart(part, "chat");
|
|
1417
|
+
return file;
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
function compileAnthropicMessages(request) {
|
|
1421
|
+
assertExclusiveInput(request);
|
|
1422
|
+
return dialogueMessages(request).map(compileAnthropicMessage);
|
|
1423
|
+
}
|
|
1424
|
+
function compileAnthropicMessage(message) {
|
|
1425
|
+
if (message.role === "tool") {
|
|
1426
|
+
return {
|
|
1427
|
+
role: "user",
|
|
1428
|
+
content: [{
|
|
1429
|
+
type: "tool_result",
|
|
1430
|
+
tool_use_id: message.toolCallId ?? message.name ?? "tool",
|
|
1431
|
+
content: contentToText(message.content)
|
|
1432
|
+
}]
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
1436
|
+
return { role, content: compileAnthropicContent(message.content) };
|
|
1437
|
+
}
|
|
1438
|
+
function compileAnthropicContent(content) {
|
|
1439
|
+
if (typeof content === "string") return content;
|
|
1440
|
+
return content.map((part) => {
|
|
1441
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
1442
|
+
if (part.type === "image_url") return anthropicImage(part.imageUrl);
|
|
1443
|
+
return anthropicFile(part);
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function dialogueMessages(request) {
|
|
1447
|
+
const source = request.messages?.length ? request.messages.filter((message) => message.role !== "system") : messagesFromInput(request);
|
|
1448
|
+
if (typeof request.prompt === "string" && request.messages?.length) {
|
|
1449
|
+
return [...source, { role: "user", content: request.prompt }];
|
|
1450
|
+
}
|
|
1451
|
+
if (!source.length && typeof request.prompt === "string") return [{ role: "user", content: request.prompt }];
|
|
1452
|
+
return source;
|
|
1453
|
+
}
|
|
1454
|
+
function messagesFromInput(request) {
|
|
1455
|
+
if (typeof request.input === "string") return [{ role: "user", content: request.input }];
|
|
1456
|
+
if (!Array.isArray(request.input)) return [];
|
|
1457
|
+
const messages = [];
|
|
1458
|
+
for (const item of request.input) {
|
|
1459
|
+
if (typeof item === "string") {
|
|
1460
|
+
messages.push({ role: "user", content: item });
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (!isRecord5(item)) continue;
|
|
1464
|
+
const role = item.role === "assistant" || item.role === "tool" || item.role === "system" ? item.role : "user";
|
|
1465
|
+
if (role === "system") continue;
|
|
1466
|
+
const content = inputContent(item.content);
|
|
1467
|
+
if (content === void 0) continue;
|
|
1468
|
+
const message = { role, content };
|
|
1469
|
+
if (typeof item.toolCallId === "string") message.toolCallId = item.toolCallId;
|
|
1470
|
+
if (typeof item.name === "string") message.name = item.name;
|
|
1471
|
+
messages.push(message);
|
|
1472
|
+
}
|
|
1473
|
+
return messages;
|
|
1474
|
+
}
|
|
1475
|
+
function inputContent(content) {
|
|
1476
|
+
if (typeof content === "string") return content;
|
|
1477
|
+
if (!Array.isArray(content)) return void 0;
|
|
1478
|
+
const parts = [];
|
|
1479
|
+
for (const part of content) {
|
|
1480
|
+
if (!isRecord5(part)) continue;
|
|
1481
|
+
if ((part.type === "text" || part.type === "input_text") && typeof part.text === "string") {
|
|
1482
|
+
parts.push({ type: "text", text: part.text });
|
|
1483
|
+
} else if (part.type === "image_url" && typeof part.imageUrl === "string") {
|
|
1484
|
+
parts.push({ type: "image_url", imageUrl: part.imageUrl });
|
|
1485
|
+
} else if (part.type === "image_url" && isRecord5(part.image_url) && typeof part.image_url.url === "string") {
|
|
1486
|
+
parts.push({ type: "image_url", imageUrl: part.image_url.url });
|
|
1487
|
+
} else if (part.type === "input_image" && typeof part.image_url === "string") {
|
|
1488
|
+
parts.push({ type: "image_url", imageUrl: part.image_url });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return parts.length ? parts : void 0;
|
|
1492
|
+
}
|
|
1493
|
+
function assertExclusiveInput(request) {
|
|
1494
|
+
const hasMessages2 = Array.isArray(request.messages) && request.messages.length > 0;
|
|
1495
|
+
if (hasMessages2 && request.input !== void 0) {
|
|
1496
|
+
throw new AiDispatcherError(
|
|
1497
|
+
"UNSUPPORTED_REQUEST_FIELD",
|
|
1498
|
+
"Cloudflare chat, messages, and run accept messages or input, not both."
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function compileFilePart(part, dialect) {
|
|
1503
|
+
if (part.type !== "file") {
|
|
1504
|
+
throw new AiDispatcherError("UNSUPPORTED_REQUEST_FIELD", "File content parts need a url or data.");
|
|
1505
|
+
}
|
|
1506
|
+
if (!part.url && !part.data) {
|
|
1507
|
+
throw new AiDispatcherError("UNSUPPORTED_REQUEST_FIELD", "File content parts need a url or data.");
|
|
1508
|
+
}
|
|
1509
|
+
if (dialect === "responses") {
|
|
1510
|
+
if (part.url) return clean({ type: "input_file", file_url: part.url, filename: part.fileName });
|
|
1511
|
+
return clean({ type: "input_file", file_data: part.data, filename: part.fileName });
|
|
1512
|
+
}
|
|
1513
|
+
return {
|
|
1514
|
+
type: "file",
|
|
1515
|
+
file: clean({
|
|
1516
|
+
filename: part.fileName,
|
|
1517
|
+
...part.url ? { url: part.url } : {},
|
|
1518
|
+
...part.data ? { file_data: part.data } : {}
|
|
1519
|
+
})
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function anthropicImage(imageUrl) {
|
|
1523
|
+
const data = dataUrl(imageUrl);
|
|
1524
|
+
if (data) return { type: "image", source: { type: "base64", media_type: data.mediaType, data: data.data } };
|
|
1525
|
+
return { type: "image", source: { type: "url", url: imageUrl } };
|
|
1526
|
+
}
|
|
1527
|
+
function anthropicFile(part) {
|
|
1528
|
+
if (part.type !== "file" || !part.url && !part.data) {
|
|
1529
|
+
throw new AiDispatcherError("UNSUPPORTED_REQUEST_FIELD", "File content parts need a url or data.");
|
|
1530
|
+
}
|
|
1531
|
+
if (part.url) {
|
|
1532
|
+
const data = dataUrl(part.url);
|
|
1533
|
+
if (data) return { type: "document", source: { type: "base64", media_type: data.mediaType, data: data.data } };
|
|
1534
|
+
return { type: "document", source: { type: "url", url: part.url } };
|
|
1535
|
+
}
|
|
1536
|
+
return {
|
|
1537
|
+
type: "document",
|
|
1538
|
+
source: {
|
|
1539
|
+
type: "base64",
|
|
1540
|
+
media_type: part.mimeType || "application/octet-stream",
|
|
1541
|
+
data: part.data
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function dataUrl(value) {
|
|
1546
|
+
const match = /^data:([^;,]+);base64,([\s\S]+)$/i.exec(value);
|
|
1547
|
+
if (!match) return void 0;
|
|
1548
|
+
return { mediaType: match[1] ?? "application/octet-stream", data: match[2] ?? "" };
|
|
1549
|
+
}
|
|
1550
|
+
function compileResponsesTools(request) {
|
|
1551
|
+
const tools = request.functionTools ?? [];
|
|
1552
|
+
if (!tools.length || request.toolChoice === "none") return void 0;
|
|
1553
|
+
return tools.map((tool) => ({
|
|
1554
|
+
type: "function",
|
|
1555
|
+
name: tool.name,
|
|
1556
|
+
description: tool.description,
|
|
1557
|
+
parameters: tool.parameters
|
|
1558
|
+
}));
|
|
1559
|
+
}
|
|
1560
|
+
function compileChatTools(request) {
|
|
1561
|
+
const tools = request.functionTools ?? [];
|
|
1562
|
+
if (!tools.length || request.toolChoice === "none") return void 0;
|
|
1563
|
+
return tools.map((tool) => ({
|
|
1564
|
+
type: "function",
|
|
1565
|
+
function: {
|
|
1566
|
+
name: tool.name,
|
|
1567
|
+
description: tool.description,
|
|
1568
|
+
parameters: tool.parameters
|
|
1569
|
+
}
|
|
1570
|
+
}));
|
|
1571
|
+
}
|
|
1572
|
+
function compileAnthropicTools(request) {
|
|
1573
|
+
const tools = request.functionTools ?? [];
|
|
1574
|
+
if (!tools.length || request.toolChoice === "none") return void 0;
|
|
1575
|
+
return tools.map((tool) => ({
|
|
1576
|
+
name: tool.name,
|
|
1577
|
+
description: tool.description,
|
|
1578
|
+
input_schema: tool.parameters
|
|
1579
|
+
}));
|
|
1580
|
+
}
|
|
1581
|
+
function compileResponsesToolChoice(request) {
|
|
1582
|
+
if (!request.functionTools?.length || request.toolChoice === "none") return void 0;
|
|
1583
|
+
const choice = request.toolChoice;
|
|
1584
|
+
if (!choice || choice === "auto") return void 0;
|
|
1585
|
+
if (choice === "required") return "required";
|
|
1586
|
+
if (typeof choice === "object" && choice.type === "function") return { type: "function", name: choice.functionName };
|
|
1587
|
+
return void 0;
|
|
1588
|
+
}
|
|
1589
|
+
function compileChatToolChoice(request) {
|
|
1590
|
+
if (!request.functionTools?.length || request.toolChoice === "none") return void 0;
|
|
1591
|
+
const choice = request.toolChoice;
|
|
1592
|
+
if (!choice || choice === "auto") return void 0;
|
|
1593
|
+
if (choice === "required") return "required";
|
|
1594
|
+
if (typeof choice === "object" && choice.type === "function") {
|
|
1595
|
+
return { type: "function", function: { name: choice.functionName } };
|
|
1596
|
+
}
|
|
1597
|
+
return void 0;
|
|
1598
|
+
}
|
|
1599
|
+
function compileAnthropicToolChoice(request) {
|
|
1600
|
+
if (!request.functionTools?.length || request.toolChoice === "none") return void 0;
|
|
1601
|
+
const choice = request.toolChoice;
|
|
1602
|
+
if (!choice || choice === "auto") return void 0;
|
|
1603
|
+
if (choice === "required") return { type: "any" };
|
|
1604
|
+
if (typeof choice === "object" && choice.type === "function") return { type: "tool", name: choice.functionName };
|
|
1605
|
+
return void 0;
|
|
1606
|
+
}
|
|
1607
|
+
function contentToText(content) {
|
|
1608
|
+
if (typeof content === "string") return content;
|
|
1609
|
+
return content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("");
|
|
1610
|
+
}
|
|
1611
|
+
async function runCloudflare(request, options, call, requestId) {
|
|
1612
|
+
const maxIterations = request.execution?.maxToolIterations ?? 8;
|
|
1613
|
+
const maxFunctionToolCalls = request.execution?.maxFunctionToolCalls ?? 20;
|
|
1614
|
+
const functionUsage = [];
|
|
1615
|
+
let active;
|
|
1616
|
+
try {
|
|
1617
|
+
active = compileCloudflareRequest(request, options, call, false);
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
if (error instanceof AiDispatcherError && isConfigError(error.code)) throw error;
|
|
1620
|
+
return failedResponse(request, requestId, error, "chat");
|
|
1621
|
+
}
|
|
1622
|
+
let response;
|
|
1623
|
+
try {
|
|
1624
|
+
for (let iteration = 0; iteration <= maxIterations; iteration += 1) {
|
|
1625
|
+
response = await sendWithRetries(active, options, timeoutFor(request, options));
|
|
1626
|
+
if (active.endpoint === "run" && !chatCalls(unwrap(response)).length) break;
|
|
1627
|
+
const calls = extractCalls(active.endpoint, response);
|
|
1628
|
+
if (!calls.length) break;
|
|
1629
|
+
if (!calls.every((item) => resolveExecutor(item.name, request, options))) {
|
|
1630
|
+
return normalizeCloudflareResponse({
|
|
1631
|
+
request,
|
|
1632
|
+
requestId,
|
|
1633
|
+
compiled: active,
|
|
1634
|
+
response,
|
|
1635
|
+
functionTools: [...functionUsage, ...calls],
|
|
1636
|
+
status: "requires_action"
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
if (functionUsage.length + calls.length > maxFunctionToolCalls) {
|
|
1640
|
+
throw new AiDispatcherError("FUNCTION_TOOL_LIMIT", "maxFunctionToolCalls exceeded.");
|
|
1641
|
+
}
|
|
1642
|
+
const results = [];
|
|
1643
|
+
for (const item of calls) results.push(await executeTool(item, request, options));
|
|
1644
|
+
functionUsage.push(...results);
|
|
1645
|
+
active = { ...active, body: continueBody(active, response, results) };
|
|
1646
|
+
}
|
|
1647
|
+
return normalizeCloudflareResponse({
|
|
1648
|
+
request,
|
|
1649
|
+
requestId,
|
|
1650
|
+
compiled: active,
|
|
1651
|
+
response,
|
|
1652
|
+
functionTools: functionUsage,
|
|
1653
|
+
status: "completed"
|
|
1654
|
+
});
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
if (error instanceof AiDispatcherError && isConfigError(error.code)) throw error;
|
|
1657
|
+
return failedResponse(request, requestId, error, responseApiMode(active.endpoint));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function continueBody(compiled, response, results) {
|
|
1661
|
+
if (compiled.endpoint === "responses") return continueResponses(compiled.body, response, results);
|
|
1662
|
+
if (compiled.endpoint === "messages") return continueMessages(compiled.body, response, results);
|
|
1663
|
+
if (compiled.endpoint === "run") return continueRun(compiled.body, response, results);
|
|
1664
|
+
return continueChat(compiled.body, response, results);
|
|
1665
|
+
}
|
|
1666
|
+
function continueResponses(body, response, results) {
|
|
1667
|
+
const record2 = unwrap(response);
|
|
1668
|
+
return clean({
|
|
1669
|
+
...body,
|
|
1670
|
+
tool_choice: "auto",
|
|
1671
|
+
previous_response_id: typeof record2.id === "string" ? record2.id : void 0,
|
|
1672
|
+
input: results.map(toFunctionCallOutput)
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
function continueChat(body, response, results) {
|
|
1676
|
+
const messages = Array.isArray(body.messages) ? [...body.messages] : [];
|
|
1677
|
+
const assistant = chatAssistantMessage(unwrap(response));
|
|
1678
|
+
if (assistant) messages.push(assistant);
|
|
1679
|
+
for (const result of results) {
|
|
1680
|
+
messages.push({
|
|
1681
|
+
role: "tool",
|
|
1682
|
+
tool_call_id: result.callId,
|
|
1683
|
+
content: outputText(result)
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
return { ...body, messages, ...body.tool_choice !== void 0 ? { tool_choice: "auto" } : {} };
|
|
1687
|
+
}
|
|
1688
|
+
function continueMessages(body, response, results) {
|
|
1689
|
+
const messages = Array.isArray(body.messages) ? [...body.messages] : [];
|
|
1690
|
+
const record2 = unwrap(response);
|
|
1691
|
+
messages.push({ role: "assistant", content: Array.isArray(record2.content) ? record2.content : [] });
|
|
1692
|
+
messages.push({
|
|
1693
|
+
role: "user",
|
|
1694
|
+
content: results.map((result) => ({
|
|
1695
|
+
type: "tool_result",
|
|
1696
|
+
tool_use_id: result.callId,
|
|
1697
|
+
content: outputText(result),
|
|
1698
|
+
...result.status === "failed" ? { is_error: true } : {}
|
|
1699
|
+
}))
|
|
1700
|
+
});
|
|
1701
|
+
return { ...body, messages };
|
|
1702
|
+
}
|
|
1703
|
+
function continueRun(body, response, results) {
|
|
1704
|
+
const input = isRecord5(body.input) ? body.input : {};
|
|
1705
|
+
const continued = continueChat({ messages: input.messages }, response, results);
|
|
1706
|
+
return { ...body, input: { ...input, messages: continued.messages } };
|
|
1707
|
+
}
|
|
1708
|
+
function toFunctionCallOutput(result) {
|
|
1709
|
+
return {
|
|
1710
|
+
type: "function_call_output",
|
|
1711
|
+
call_id: result.callId,
|
|
1712
|
+
output: outputText(result)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function outputText(result) {
|
|
1716
|
+
const output = result.status === "completed" ? result.result : { error: result.error };
|
|
1717
|
+
return typeof output === "string" ? output : JSON.stringify(output);
|
|
1718
|
+
}
|
|
1719
|
+
function resolveExecutor(name, request, options) {
|
|
1720
|
+
return request.functionTools?.find((tool) => tool.name === name)?.executor ?? options.tools?.[name];
|
|
1721
|
+
}
|
|
1722
|
+
async function executeTool(call, request, options) {
|
|
1723
|
+
const executor = resolveExecutor(call.name, request, options);
|
|
1724
|
+
if (!executor) return call;
|
|
1725
|
+
const started = Date.now();
|
|
1726
|
+
try {
|
|
1727
|
+
options.logger?.debug?.("runtime.function_tool.started", { name: call.name, callId: call.callId });
|
|
1728
|
+
const result = await executor(call.args, {
|
|
1729
|
+
requestId: request.id ?? "",
|
|
1730
|
+
toolName: call.name,
|
|
1731
|
+
callId: call.callId,
|
|
1732
|
+
...request.metadata ? { metadata: request.metadata } : {}
|
|
1733
|
+
});
|
|
1734
|
+
const usage = {
|
|
1735
|
+
name: call.name,
|
|
1736
|
+
callId: call.callId,
|
|
1737
|
+
status: "completed",
|
|
1738
|
+
args: call.args,
|
|
1739
|
+
result,
|
|
1740
|
+
durationMs: Date.now() - started
|
|
1741
|
+
};
|
|
1742
|
+
options.logger?.debug?.("runtime.function_tool.completed", { name: call.name, callId: call.callId });
|
|
1743
|
+
return usage;
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
const usage = {
|
|
1746
|
+
name: call.name,
|
|
1747
|
+
callId: call.callId,
|
|
1748
|
+
status: "failed",
|
|
1749
|
+
args: call.args,
|
|
1750
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1751
|
+
durationMs: Date.now() - started
|
|
1752
|
+
};
|
|
1753
|
+
options.logger?.warn?.("runtime.function_tool.failed", usage);
|
|
1754
|
+
return usage;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function* streamCloudflare(request, options, call) {
|
|
1758
|
+
const requestId = request.id ?? "";
|
|
1759
|
+
let compiled;
|
|
1760
|
+
try {
|
|
1761
|
+
compiled = compileCloudflareRequest(request, options, call, true);
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
yield errorEvent(requestId, error);
|
|
1764
|
+
throw error;
|
|
1765
|
+
}
|
|
1766
|
+
yield {
|
|
1767
|
+
type: "stream.start",
|
|
1768
|
+
requestId,
|
|
1769
|
+
data: {
|
|
1770
|
+
model: modelOf(compiled),
|
|
1771
|
+
apiMode: responseApiMode(compiled.endpoint),
|
|
1772
|
+
entrypoint: "executeStreamingChat"
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
const controller = new AbortController();
|
|
1776
|
+
const timeout = setTimeout(() => controller.abort(), timeoutFor(request, options));
|
|
1777
|
+
let text = "";
|
|
1778
|
+
let reported = false;
|
|
1779
|
+
try {
|
|
1780
|
+
const response = await options.fetch(compiled.url, {
|
|
1781
|
+
method: "POST",
|
|
1782
|
+
headers: { ...compiled.headers, Accept: "text/event-stream" },
|
|
1783
|
+
body: JSON.stringify(compiled.body),
|
|
1784
|
+
signal: controller.signal
|
|
1785
|
+
});
|
|
1786
|
+
if (!response.ok) {
|
|
1787
|
+
const failure = await readFailure(response);
|
|
1788
|
+
reported = true;
|
|
1789
|
+
yield errorEvent(requestId, failure);
|
|
1790
|
+
throw failure;
|
|
1791
|
+
}
|
|
1792
|
+
for await (const event of parseSse(response)) {
|
|
1793
|
+
if (!isRecord5(event)) continue;
|
|
1794
|
+
const catalogReasoning = call?.extractStreamReasoning?.(event);
|
|
1795
|
+
const reasoningText = catalogReasoning || reasoningDelta(event);
|
|
1796
|
+
if (reasoningText) yield { type: "stream.reasoning.delta", requestId, data: { text: reasoningText } };
|
|
1797
|
+
const delta = textDelta(event);
|
|
1798
|
+
if (delta) {
|
|
1799
|
+
text += delta;
|
|
1800
|
+
yield { type: "stream.text.delta", requestId, data: { text: delta } };
|
|
1801
|
+
}
|
|
1802
|
+
const toolDelta = toolCallDelta(event);
|
|
1803
|
+
if (toolDelta) yield { type: "stream.tool_call.delta", requestId, data: toolDelta };
|
|
1804
|
+
const usage = streamUsage(event);
|
|
1805
|
+
if (usage) yield { type: "stream.usage", requestId, data: { usage } };
|
|
1806
|
+
if (event.type === "error") {
|
|
1807
|
+
const failure = new AiDispatcherError(
|
|
1808
|
+
"PROVIDER_REQUEST_FAILED",
|
|
1809
|
+
typeof event.message === "string" ? event.message : "Cloudflare stream returned an error event.",
|
|
1810
|
+
event
|
|
1811
|
+
);
|
|
1812
|
+
reported = true;
|
|
1813
|
+
yield errorEvent(requestId, failure);
|
|
1814
|
+
throw failure;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
yield { type: "stream.done", requestId, data: { text, model: modelOf(compiled) } };
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
const failure = error instanceof Error && error.name === "AbortError" ? new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", "Cloudflare request timed out.") : error instanceof AiDispatcherError ? error : new AiDispatcherError(
|
|
1820
|
+
"PROVIDER_GATEWAY_UNREACHABLE",
|
|
1821
|
+
error instanceof Error ? error.message : "Cloudflare request failed before receiving a response."
|
|
1822
|
+
);
|
|
1823
|
+
if (!reported) yield errorEvent(requestId, failure);
|
|
1824
|
+
throw failure;
|
|
1825
|
+
} finally {
|
|
1826
|
+
clearTimeout(timeout);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
async function sendWithRetries(compiled, options, timeoutMs) {
|
|
1830
|
+
let lastError;
|
|
1831
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
|
|
1832
|
+
try {
|
|
1833
|
+
return await sendOnce(compiled, options, timeoutMs);
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
lastError = error;
|
|
1836
|
+
const retryable = error instanceof AiDispatcherError && error.code === "PROVIDER_RETRYABLE";
|
|
1837
|
+
if (!retryable || attempt >= options.maxAttempts) throw asTerminalError(error);
|
|
1838
|
+
await delay(Math.min(500 * 2 ** (attempt - 1), 5e3));
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
throw lastError;
|
|
1842
|
+
}
|
|
1843
|
+
async function sendOnce(compiled, options, timeoutMs) {
|
|
1844
|
+
const controller = new AbortController();
|
|
1845
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1846
|
+
try {
|
|
1847
|
+
const response = await options.fetch(compiled.url, {
|
|
1848
|
+
method: "POST",
|
|
1849
|
+
headers: compiled.headers,
|
|
1850
|
+
body: JSON.stringify(compiled.body),
|
|
1851
|
+
signal: controller.signal
|
|
1852
|
+
});
|
|
1853
|
+
if (!response.ok) throw await readFailure(response);
|
|
1854
|
+
const text = await response.text();
|
|
1855
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
1856
|
+
if (isRecord5(parsed) && parsed.success === false) {
|
|
1857
|
+
throw new AiDispatcherError(
|
|
1858
|
+
"PROVIDER_REQUEST_FAILED",
|
|
1859
|
+
messageFrom(parsed) ?? "Cloudflare request failed.",
|
|
1860
|
+
{ body: parsed }
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
return parsed;
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
if (error instanceof AiDispatcherError) throw error;
|
|
1866
|
+
if (error instanceof SyntaxError) {
|
|
1867
|
+
throw new AiDispatcherError("PROVIDER_REQUEST_FAILED", "Cloudflare returned a response that was not JSON.");
|
|
1868
|
+
}
|
|
1869
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1870
|
+
throw new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", "Cloudflare request timed out.");
|
|
1871
|
+
}
|
|
1872
|
+
throw new AiDispatcherError(
|
|
1873
|
+
"PROVIDER_GATEWAY_UNREACHABLE",
|
|
1874
|
+
error instanceof Error ? error.message : "Cloudflare request failed before receiving a response."
|
|
1875
|
+
);
|
|
1876
|
+
} finally {
|
|
1877
|
+
clearTimeout(timeout);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
async function readFailure(response) {
|
|
1881
|
+
const text = await response.text();
|
|
1882
|
+
let body = text;
|
|
1883
|
+
try {
|
|
1884
|
+
body = text ? JSON.parse(text) : void 0;
|
|
1885
|
+
} catch {
|
|
1886
|
+
body = text;
|
|
1887
|
+
}
|
|
1888
|
+
const message = messageFrom(body) ?? `Cloudflare request failed with status ${response.status}.`;
|
|
1889
|
+
const retryable = response.status === 429 || response.status === 408 || response.status >= 500;
|
|
1890
|
+
return new AiDispatcherError(classifyStatus(response.status), message, {
|
|
1891
|
+
status: response.status,
|
|
1892
|
+
body,
|
|
1893
|
+
retryable
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
function classifyStatus(status) {
|
|
1897
|
+
if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
|
|
1898
|
+
if (status === 404) return "PROVIDER_MODEL_NOT_FOUND";
|
|
1899
|
+
if (status === 429 || status === 408 || status >= 500) return "PROVIDER_RETRYABLE";
|
|
1900
|
+
return "PROVIDER_REQUEST_FAILED";
|
|
1901
|
+
}
|
|
1902
|
+
function isConfigError(code) {
|
|
1903
|
+
return code === "CLOUDFLARE_API_TOKEN_MISSING" || code === "CLOUDFLARE_ACCOUNT_ID_MISSING" || code === "MODEL_REQUIRED" || code === "INPUT_REQUIRED" || code === "UNSUPPORTED_REQUEST_FIELD";
|
|
1904
|
+
}
|
|
1905
|
+
function asTerminalError(error) {
|
|
1906
|
+
if (error instanceof AiDispatcherError && error.code === "PROVIDER_RETRYABLE") {
|
|
1907
|
+
const status = isRecord5(error.details) ? error.details.status : void 0;
|
|
1908
|
+
if (status === 429) return new AiDispatcherError("PROVIDER_RATE_LIMITED", error.message, error.details);
|
|
1909
|
+
return new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", error.message, error.details);
|
|
1910
|
+
}
|
|
1911
|
+
if (error instanceof AiDispatcherError) return error;
|
|
1912
|
+
return new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
1913
|
+
}
|
|
1914
|
+
function normalizeCloudflareResponse(params) {
|
|
1915
|
+
const payload = unwrap(params.response);
|
|
1916
|
+
const calls = params.functionTools ?? extractCalls(params.compiled.endpoint, params.response);
|
|
1917
|
+
const usage = normalizeUsage(payload.usage) ?? normalizeUsage(isRecord5(params.response) ? params.response.usage : void 0);
|
|
1918
|
+
const status = params.status ?? (calls.length ? "requires_action" : "completed");
|
|
1919
|
+
return {
|
|
1920
|
+
id: typeof payload.id === "string" ? payload.id : params.requestId,
|
|
1921
|
+
status,
|
|
1922
|
+
apiMode: responseApiMode(params.compiled.endpoint),
|
|
1923
|
+
model: typeof payload.model === "string" ? payload.model : modelOf(params.compiled),
|
|
1924
|
+
text: answerText(payload),
|
|
1925
|
+
citations: [],
|
|
1926
|
+
images: [],
|
|
1927
|
+
patches: [],
|
|
1928
|
+
toolUsage: emptyToolUsage(calls),
|
|
1929
|
+
...usage ? { usage } : {},
|
|
1930
|
+
warnings: params.compiled.warnings,
|
|
1931
|
+
errors: [],
|
|
1932
|
+
raw: { request: params.compiled.body, response: params.response },
|
|
1933
|
+
...params.request.metadata ? { metadata: params.request.metadata } : {}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function responseApiMode(endpoint) {
|
|
1937
|
+
return endpoint === "responses" ? "responses" : "chat";
|
|
1938
|
+
}
|
|
1939
|
+
function answerText(response) {
|
|
1940
|
+
if (typeof response.output_text === "string") return response.output_text;
|
|
1941
|
+
if (typeof response.text === "string" && !Array.isArray(response.choices) && !Array.isArray(response.content)) {
|
|
1942
|
+
return response.text;
|
|
1943
|
+
}
|
|
1944
|
+
const responses = responsesAnswer(response);
|
|
1945
|
+
if (responses) return responses;
|
|
1946
|
+
const chat = chatAnswer(response);
|
|
1947
|
+
if (chat) return chat;
|
|
1948
|
+
const anthropic = anthropicAnswer(response);
|
|
1949
|
+
if (anthropic) return anthropic;
|
|
1950
|
+
if (isRecord5(response.result)) return answerText(response.result);
|
|
1951
|
+
return "";
|
|
1952
|
+
}
|
|
1953
|
+
function responsesAnswer(response) {
|
|
1954
|
+
if (!Array.isArray(response.output)) return "";
|
|
1955
|
+
const parts = [];
|
|
1956
|
+
for (const item of response.output) {
|
|
1957
|
+
if (!isRecord5(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
1958
|
+
for (const part of item.content) {
|
|
1959
|
+
if (isRecord5(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return parts.join("");
|
|
1963
|
+
}
|
|
1964
|
+
function chatAnswer(response) {
|
|
1965
|
+
const message = chatAssistantMessage(response);
|
|
1966
|
+
if (!message) return "";
|
|
1967
|
+
const content = message.content;
|
|
1968
|
+
if (typeof content === "string") return content;
|
|
1969
|
+
if (!Array.isArray(content)) return "";
|
|
1970
|
+
return content.map((part) => isRecord5(part) && typeof part.text === "string" ? part.text : "").filter(Boolean).join("");
|
|
1971
|
+
}
|
|
1972
|
+
function anthropicAnswer(response) {
|
|
1973
|
+
if (!Array.isArray(response.content)) return "";
|
|
1974
|
+
return response.content.map((part) => isRecord5(part) && part.type === "text" && typeof part.text === "string" ? part.text : "").filter(Boolean).join("");
|
|
1975
|
+
}
|
|
1976
|
+
function chatAssistantMessage(response) {
|
|
1977
|
+
if (!Array.isArray(response.choices) || !isRecord5(response.choices[0])) return void 0;
|
|
1978
|
+
const message = response.choices[0].message;
|
|
1979
|
+
return isRecord5(message) ? message : void 0;
|
|
1980
|
+
}
|
|
1981
|
+
function extractCalls(endpoint, response) {
|
|
1982
|
+
const record2 = unwrap(response);
|
|
1983
|
+
if (endpoint === "responses") return responsesCalls(record2);
|
|
1984
|
+
if (endpoint === "messages") return anthropicCalls(record2);
|
|
1985
|
+
return chatCalls(record2);
|
|
1986
|
+
}
|
|
1987
|
+
function responsesCalls(response) {
|
|
1988
|
+
if (!Array.isArray(response.output)) return [];
|
|
1989
|
+
const calls = [];
|
|
1990
|
+
for (const item of response.output) {
|
|
1991
|
+
if (!isRecord5(item) || item.type !== "function_call") continue;
|
|
1992
|
+
calls.push(callUsage(
|
|
1993
|
+
typeof item.name === "string" ? item.name : "function",
|
|
1994
|
+
typeof item.call_id === "string" ? item.call_id : "function",
|
|
1995
|
+
item.arguments
|
|
1996
|
+
));
|
|
1997
|
+
}
|
|
1998
|
+
return calls;
|
|
1999
|
+
}
|
|
2000
|
+
function chatCalls(response) {
|
|
2001
|
+
const message = chatAssistantMessage(response);
|
|
2002
|
+
if (!message || !Array.isArray(message.tool_calls)) return [];
|
|
2003
|
+
const calls = [];
|
|
2004
|
+
for (const item of message.tool_calls) {
|
|
2005
|
+
if (!isRecord5(item) || !isRecord5(item.function)) continue;
|
|
2006
|
+
const name = typeof item.function.name === "string" ? item.function.name : "function";
|
|
2007
|
+
calls.push(callUsage(
|
|
2008
|
+
name,
|
|
2009
|
+
typeof item.id === "string" ? item.id : name,
|
|
2010
|
+
item.function.arguments
|
|
2011
|
+
));
|
|
2012
|
+
}
|
|
2013
|
+
return calls;
|
|
2014
|
+
}
|
|
2015
|
+
function anthropicCalls(response) {
|
|
2016
|
+
if (!Array.isArray(response.content)) return [];
|
|
2017
|
+
const calls = [];
|
|
2018
|
+
for (const item of response.content) {
|
|
2019
|
+
if (!isRecord5(item) || item.type !== "tool_use") continue;
|
|
2020
|
+
const name = typeof item.name === "string" ? item.name : "function";
|
|
2021
|
+
calls.push({
|
|
2022
|
+
name,
|
|
2023
|
+
callId: typeof item.id === "string" ? item.id : name,
|
|
2024
|
+
status: "completed",
|
|
2025
|
+
args: item.input
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
return calls;
|
|
2029
|
+
}
|
|
2030
|
+
function callUsage(name, callId, rawArgs) {
|
|
2031
|
+
let args = rawArgs;
|
|
2032
|
+
if (typeof rawArgs === "string") {
|
|
2033
|
+
try {
|
|
2034
|
+
args = JSON.parse(rawArgs);
|
|
2035
|
+
} catch {
|
|
2036
|
+
args = rawArgs;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return { name, callId, status: "completed", args };
|
|
2040
|
+
}
|
|
2041
|
+
function emptyToolUsage(calls) {
|
|
2042
|
+
const unused = { requested: false, required: false, used: false };
|
|
2043
|
+
return {
|
|
2044
|
+
serverTools: {
|
|
2045
|
+
webSearch: unused,
|
|
2046
|
+
webFetch: unused,
|
|
2047
|
+
datetime: unused,
|
|
2048
|
+
imageGeneration: unused,
|
|
2049
|
+
applyPatch: unused,
|
|
2050
|
+
fusion: unused,
|
|
2051
|
+
advisor: unused,
|
|
2052
|
+
subagent: unused
|
|
2053
|
+
},
|
|
2054
|
+
functionTools: calls
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function normalizeUsage(usage) {
|
|
2058
|
+
if (!isRecord5(usage)) return void 0;
|
|
2059
|
+
const inputTokens = numberValue(usage.input_tokens) ?? numberValue(usage.prompt_tokens);
|
|
2060
|
+
const outputTokens = numberValue(usage.output_tokens) ?? numberValue(usage.completion_tokens);
|
|
2061
|
+
const totalTokens = numberValue(usage.total_tokens) ?? (inputTokens !== void 0 || outputTokens !== void 0 ? (inputTokens ?? 0) + (outputTokens ?? 0) : void 0);
|
|
2062
|
+
if (inputTokens === void 0 && outputTokens === void 0 && totalTokens === void 0) return { raw: usage };
|
|
2063
|
+
return {
|
|
2064
|
+
raw: usage,
|
|
2065
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
2066
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
2067
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
function failedResponse(request, requestId, error, apiMode) {
|
|
2071
|
+
const normalized = error instanceof AiDispatcherError ? error : new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
2072
|
+
return {
|
|
2073
|
+
id: requestId,
|
|
2074
|
+
status: "failed",
|
|
2075
|
+
apiMode,
|
|
2076
|
+
model: request.model ?? "",
|
|
2077
|
+
text: "",
|
|
2078
|
+
citations: [],
|
|
2079
|
+
images: [],
|
|
2080
|
+
patches: [],
|
|
2081
|
+
toolUsage: emptyToolUsage([]),
|
|
2082
|
+
warnings: [],
|
|
2083
|
+
errors: [{
|
|
2084
|
+
code: normalized.code,
|
|
2085
|
+
message: normalized.message,
|
|
2086
|
+
source: "runtime",
|
|
2087
|
+
details: normalized.details
|
|
2088
|
+
}],
|
|
2089
|
+
raw: { request, response: void 0 }
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function errorEvent(requestId, error) {
|
|
2093
|
+
const normalized = error instanceof AiDispatcherError ? error : new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
2094
|
+
return {
|
|
2095
|
+
type: "stream.error",
|
|
2096
|
+
requestId,
|
|
2097
|
+
data: {
|
|
2098
|
+
error: {
|
|
2099
|
+
code: normalized.code,
|
|
2100
|
+
message: normalized.message,
|
|
2101
|
+
source: "runtime",
|
|
2102
|
+
details: normalized.details
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
481
2106
|
}
|
|
482
|
-
function
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"An MCP server needs an id, a tools array, and a callTool function."
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
if (seen.has(server.id)) {
|
|
490
|
-
throw new AiDispatcherError("MCP_SERVER_INVALID", `MCP server "${server.id}" is listed more than once.`);
|
|
2107
|
+
function textDelta(event) {
|
|
2108
|
+
if (event.type === "response.output_text.delta" && typeof event.delta === "string" && event.delta) return event.delta;
|
|
2109
|
+
if (event.type === "content_block_delta" && isRecord5(event.delta) && event.delta.type === "text_delta" && typeof event.delta.text === "string" && event.delta.text) {
|
|
2110
|
+
return event.delta.text;
|
|
491
2111
|
}
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
2112
|
+
const choice = firstChoiceDelta(event);
|
|
2113
|
+
if (choice && typeof choice.content === "string" && choice.content) return choice.content;
|
|
2114
|
+
return void 0;
|
|
2115
|
+
}
|
|
2116
|
+
function reasoningDelta(event) {
|
|
2117
|
+
if (event.type === "response.reasoning_summary_text.delta" && typeof event.delta === "string" && event.delta) return event.delta;
|
|
2118
|
+
if (event.type === "content_block_delta" && isRecord5(event.delta) && event.delta.type === "thinking_delta" && typeof event.delta.thinking === "string" && event.delta.thinking) {
|
|
2119
|
+
return event.delta.thinking;
|
|
498
2120
|
}
|
|
2121
|
+
const choice = firstChoiceDelta(event);
|
|
2122
|
+
if (!choice) return void 0;
|
|
2123
|
+
if (typeof choice.reasoning === "string" && choice.reasoning) return choice.reasoning;
|
|
2124
|
+
if (typeof choice.reasoning_content === "string" && choice.reasoning_content) return choice.reasoning_content;
|
|
2125
|
+
return void 0;
|
|
499
2126
|
}
|
|
500
|
-
function
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
2127
|
+
function toolCallDelta(event) {
|
|
2128
|
+
if (event.type === "response.function_call_arguments.delta" && typeof event.delta === "string") {
|
|
2129
|
+
return {
|
|
2130
|
+
index: 0,
|
|
2131
|
+
...typeof event.name === "string" ? { name: event.name } : {},
|
|
2132
|
+
argumentsDelta: event.delta
|
|
2133
|
+
};
|
|
506
2134
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
throw new AiDispatcherError(
|
|
510
|
-
"MCP_TOOL_NAME_INVALID",
|
|
511
|
-
`MCP tool "${exposedName}" is longer than ${MAX_EXPOSED_NAME_LENGTH} characters.`
|
|
512
|
-
);
|
|
2135
|
+
if (event.type === "content_block_delta" && isRecord5(event.delta) && event.delta.type === "input_json_delta" && typeof event.delta.partial_json === "string") {
|
|
2136
|
+
return { index: typeof event.index === "number" ? event.index : 0, argumentsDelta: event.delta.partial_json };
|
|
513
2137
|
}
|
|
514
|
-
|
|
2138
|
+
const choice = firstChoiceDelta(event);
|
|
2139
|
+
if (!choice || !Array.isArray(choice.tool_calls) || !isRecord5(choice.tool_calls[0])) return void 0;
|
|
2140
|
+
const tool = choice.tool_calls[0];
|
|
2141
|
+
const fn = isRecord5(tool.function) ? tool.function : void 0;
|
|
2142
|
+
const args = fn && typeof fn.arguments === "string" ? fn.arguments : void 0;
|
|
2143
|
+
if (!args && !(fn && typeof fn.name === "string")) return void 0;
|
|
2144
|
+
return {
|
|
2145
|
+
index: typeof tool.index === "number" ? tool.index : 0,
|
|
2146
|
+
...fn && typeof fn.name === "string" ? { name: fn.name } : {},
|
|
2147
|
+
...args ? { argumentsDelta: args } : {}
|
|
2148
|
+
};
|
|
515
2149
|
}
|
|
516
|
-
function
|
|
517
|
-
if (
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
2150
|
+
function streamUsage(event) {
|
|
2151
|
+
if (event.type === "response.completed" && isRecord5(event.response)) return normalizeUsage(event.response.usage);
|
|
2152
|
+
if (isRecord5(event.usage)) return normalizeUsage(event.usage);
|
|
2153
|
+
return void 0;
|
|
2154
|
+
}
|
|
2155
|
+
function firstChoiceDelta(event) {
|
|
2156
|
+
if (!Array.isArray(event.choices) || !isRecord5(event.choices[0])) return void 0;
|
|
2157
|
+
const delta = event.choices[0].delta;
|
|
2158
|
+
return isRecord5(delta) ? delta : void 0;
|
|
2159
|
+
}
|
|
2160
|
+
function unwrap(response) {
|
|
2161
|
+
if (!isRecord5(response)) return {};
|
|
2162
|
+
if (response.success === true && isRecord5(response.result)) return response.result;
|
|
2163
|
+
return response;
|
|
2164
|
+
}
|
|
2165
|
+
function modelOf(compiled) {
|
|
2166
|
+
if (typeof compiled.body.model === "string") return compiled.body.model;
|
|
2167
|
+
return "";
|
|
2168
|
+
}
|
|
2169
|
+
function messageFrom(body) {
|
|
2170
|
+
if (!isRecord5(body)) return void 0;
|
|
2171
|
+
if (typeof body.message === "string") return body.message;
|
|
2172
|
+
if (isRecord5(body.error) && typeof body.error.message === "string") return body.error.message;
|
|
2173
|
+
if (Array.isArray(body.errors)) {
|
|
2174
|
+
for (const error of body.errors) {
|
|
2175
|
+
if (isRecord5(error) && typeof error.message === "string") return error.message;
|
|
2176
|
+
}
|
|
523
2177
|
}
|
|
524
|
-
return
|
|
2178
|
+
return void 0;
|
|
525
2179
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
2180
|
+
function numberValue(value) {
|
|
2181
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2182
|
+
}
|
|
2183
|
+
function applyProviderRecord(body, metadata) {
|
|
2184
|
+
if (metadata) return { ...body, metadata };
|
|
2185
|
+
if (body.metadata === void 0) return body;
|
|
2186
|
+
const rest = { ...body };
|
|
2187
|
+
delete rest.metadata;
|
|
2188
|
+
return rest;
|
|
2189
|
+
}
|
|
2190
|
+
function overlayMetadata(base, extra) {
|
|
2191
|
+
if (!base) return extra;
|
|
2192
|
+
if (!extra) return base;
|
|
2193
|
+
return { ...base, ...extra };
|
|
2194
|
+
}
|
|
2195
|
+
function endpointUrl(options, endpoint) {
|
|
2196
|
+
const account = encodeURIComponent(options.accountId);
|
|
2197
|
+
const root = `${options.baseUrl}/accounts/${account}/ai`;
|
|
2198
|
+
if (endpoint === "chat") return `${root}/v1/chat/completions`;
|
|
2199
|
+
if (endpoint === "responses") return `${root}/v1/responses`;
|
|
2200
|
+
if (endpoint === "messages") return `${root}/v1/messages`;
|
|
2201
|
+
return `${root}/run`;
|
|
2202
|
+
}
|
|
2203
|
+
function buildHeaders(options, call, model, metadata) {
|
|
2204
|
+
const gatewayId = call?.gatewayId?.trim() || options.gatewayId?.trim() || (isWorkersModel(model) ? DEFAULT_GATEWAY_ID : void 0);
|
|
2205
|
+
const gateway = mergeGateway(options.gateway, call?.gateway);
|
|
2206
|
+
const cache = call?.cache;
|
|
2207
|
+
const skip = gateway?.skipCache !== void 0 ? gateway.skipCache : cache?.skip === true ? true : void 0;
|
|
2208
|
+
const ttl = gateway?.cacheTtlSeconds ?? cache?.ttlSeconds;
|
|
2209
|
+
const key = gateway?.cacheKey ?? cache?.key;
|
|
2210
|
+
return clean({
|
|
2211
|
+
"content-type": "application/json",
|
|
2212
|
+
authorization: `Bearer ${options.apiToken}`,
|
|
2213
|
+
...gatewayId ? { "cf-aig-gateway-id": gatewayId } : {},
|
|
2214
|
+
...skip !== void 0 ? { "cf-aig-skip-cache": String(skip) } : {},
|
|
2215
|
+
...ttl !== void 0 ? { "cf-aig-cache-ttl": String(ttl) } : {},
|
|
2216
|
+
...key ? { "cf-aig-cache-key": key } : {},
|
|
2217
|
+
...gateway?.collectLog !== void 0 ? { "cf-aig-collect-log": String(gateway.collectLog) } : {},
|
|
2218
|
+
...gateway?.requestTimeoutMs !== void 0 ? { "cf-aig-request-timeout": String(gateway.requestTimeoutMs) } : {},
|
|
2219
|
+
...gateway?.maxGatewayAttempts !== void 0 ? { "cf-aig-max-attempts": String(gateway.maxGatewayAttempts) } : {},
|
|
2220
|
+
...gateway?.retryDelayMs !== void 0 ? { "cf-aig-retry-delay": String(gateway.retryDelayMs) } : {},
|
|
2221
|
+
...isBackoff(gateway?.backoff) ? { "cf-aig-backoff": gateway.backoff } : {},
|
|
2222
|
+
...metadata ? { "cf-aig-metadata": JSON.stringify(metadata) } : {}
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
function mergeGateway(base, over) {
|
|
2226
|
+
if (!base && !over) return void 0;
|
|
2227
|
+
return { ...base, ...over };
|
|
2228
|
+
}
|
|
2229
|
+
function isBackoff(value) {
|
|
2230
|
+
return value === "constant" || value === "linear" || value === "exponential";
|
|
2231
|
+
}
|
|
2232
|
+
function isWorkersModel(model) {
|
|
2233
|
+
return model.trim().toLowerCase().startsWith("@cf/");
|
|
2234
|
+
}
|
|
2235
|
+
function isBareOpenAiModel(id) {
|
|
2236
|
+
return /^(gpt|o[134])(?:[.\-:]|$)/i.test(id.trim());
|
|
2237
|
+
}
|
|
2238
|
+
function timeoutFor(request, options) {
|
|
2239
|
+
return request.execution?.timeoutMs ?? options.timeoutMs;
|
|
2240
|
+
}
|
|
2241
|
+
function delay(ms) {
|
|
2242
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2243
|
+
}
|
|
2244
|
+
async function* parseSse(response) {
|
|
2245
|
+
const body = response.body;
|
|
2246
|
+
if (!body) throw new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", "Cloudflare stream response had no body.");
|
|
2247
|
+
const reader = body.getReader();
|
|
2248
|
+
const decoder = new TextDecoder();
|
|
2249
|
+
let buffer = "";
|
|
2250
|
+
while (true) {
|
|
2251
|
+
const chunk = await reader.read();
|
|
2252
|
+
if (chunk.done) break;
|
|
2253
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
2254
|
+
const parts = buffer.split("\n\n");
|
|
2255
|
+
buffer = parts.pop() ?? "";
|
|
2256
|
+
for (const part of parts) {
|
|
2257
|
+
const parsed = parseSsePart(part);
|
|
2258
|
+
if (parsed !== void 0) yield parsed;
|
|
2259
|
+
}
|
|
533
2260
|
}
|
|
534
|
-
|
|
2261
|
+
const trailing = parseSsePart(buffer);
|
|
2262
|
+
if (trailing !== void 0) yield trailing;
|
|
535
2263
|
}
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
2264
|
+
function parseSsePart(part) {
|
|
2265
|
+
const data = part.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
2266
|
+
if (!data || data === "[DONE]") return void 0;
|
|
2267
|
+
try {
|
|
2268
|
+
return JSON.parse(data);
|
|
2269
|
+
} catch {
|
|
2270
|
+
return void 0;
|
|
542
2271
|
}
|
|
543
|
-
if (!parts.length) return void 0;
|
|
544
|
-
return parts.join("\n");
|
|
545
2272
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
|
|
2273
|
+
function resolveCloudflareOptions(options) {
|
|
2274
|
+
const {
|
|
2275
|
+
apiToken,
|
|
2276
|
+
accountId,
|
|
2277
|
+
baseUrl,
|
|
2278
|
+
defaultModel,
|
|
2279
|
+
endpoint,
|
|
2280
|
+
gatewayId,
|
|
2281
|
+
timeoutMs,
|
|
2282
|
+
maxAttempts,
|
|
2283
|
+
fetch,
|
|
2284
|
+
tools,
|
|
2285
|
+
logger,
|
|
2286
|
+
run,
|
|
2287
|
+
...gateway
|
|
2288
|
+
} = options;
|
|
2289
|
+
void run;
|
|
2290
|
+
return {
|
|
2291
|
+
apiToken: apiToken ?? readEnv("CLOUDFLARE_API_TOKEN") ?? "",
|
|
2292
|
+
accountId: accountId ?? readEnv("CLOUDFLARE_ACCOUNT_ID") ?? "",
|
|
2293
|
+
baseUrl: (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
2294
|
+
...defaultModel ? { defaultModel } : {},
|
|
2295
|
+
...endpoint ? { endpoint } : {},
|
|
2296
|
+
...gatewayId ? { gatewayId } : {},
|
|
2297
|
+
...hasGateway(gateway) ? { gateway } : {},
|
|
2298
|
+
timeoutMs: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2299
|
+
maxAttempts: maxAttempts ?? 2,
|
|
2300
|
+
fetch: fetch ?? globalThis.fetch,
|
|
2301
|
+
...tools ? { tools } : {},
|
|
2302
|
+
...logger ? { logger } : {}
|
|
2303
|
+
};
|
|
549
2304
|
}
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
const message = error instanceof Error ? error.message : `MCP server "${serverId}" listTools failed.`;
|
|
553
|
-
return new AiDispatcherError("MCP_LIST_FAILED", message);
|
|
2305
|
+
function hasGateway(gateway) {
|
|
2306
|
+
return Object.values(gateway).some((value) => value !== void 0);
|
|
554
2307
|
}
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
return serialized === void 0 ? "null" : serialized;
|
|
2308
|
+
function clean(body) {
|
|
2309
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
|
|
558
2310
|
}
|
|
559
|
-
function
|
|
2311
|
+
function readEnv(name) {
|
|
2312
|
+
if (typeof process === "undefined") return void 0;
|
|
2313
|
+
return process.env[name];
|
|
2314
|
+
}
|
|
2315
|
+
function isRecord5(value) {
|
|
560
2316
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
561
2317
|
}
|
|
562
2318
|
|
|
563
2319
|
// src/prepare-request.ts
|
|
564
|
-
import {
|
|
565
|
-
AIProfilesError,
|
|
566
|
-
mergeReasoningBody,
|
|
567
|
-
resolveReasoning
|
|
568
|
-
} from "@x12i/ai-profiles";
|
|
569
2320
|
function prepareDispatchRequest(params) {
|
|
570
2321
|
const cloned = cloneRequest(params.request);
|
|
2322
|
+
applyDispatchIdentity(cloned);
|
|
2323
|
+
absorbOverrideMetadata(cloned);
|
|
571
2324
|
const provider = params.provider;
|
|
572
2325
|
const reasoningEffort = cloned.reasoningEffort;
|
|
573
2326
|
const bedrockControls = cloned.bedrockControls;
|
|
2327
|
+
const cachePolicy = cloned.cache;
|
|
2328
|
+
const cloudflareRequest = cloned.cloudflare;
|
|
574
2329
|
delete cloned.provider;
|
|
575
2330
|
delete cloned.reasoningEffort;
|
|
576
2331
|
delete cloned.bedrockControls;
|
|
2332
|
+
delete cloned.cache;
|
|
2333
|
+
delete cloned.cloudflare;
|
|
577
2334
|
if (provider === "openai") assertOpenAiConfigured(params.options);
|
|
2335
|
+
if (provider === "cloudflare") assertCloudflareConfigured(params.options);
|
|
578
2336
|
if (provider === "bedrock" && cloned.reasoning !== void 0) {
|
|
579
2337
|
if (bedrockControls?.additionalModelRequestFields === void 0 && reasoningEffort === void 0) {
|
|
580
2338
|
throw new AiDispatcherError(
|
|
@@ -587,6 +2345,7 @@ function prepareDispatchRequest(params) {
|
|
|
587
2345
|
const requestedModel = cloned.model ?? defaultModelFor(provider, params.options) ?? "";
|
|
588
2346
|
const explicit = hasExplicitControl(provider, cloned, bedrockControls);
|
|
589
2347
|
let directive;
|
|
2348
|
+
let catalogPrefix;
|
|
590
2349
|
if (!explicit && reasoningEffort !== void 0) {
|
|
591
2350
|
if (!requestedModel) {
|
|
592
2351
|
throw new AiDispatcherError(
|
|
@@ -594,26 +2353,69 @@ function prepareDispatchRequest(params) {
|
|
|
594
2353
|
"reasoningEffort requires a model id. Set request.model or the provider defaultModel."
|
|
595
2354
|
);
|
|
596
2355
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
effort: reasoningEffort
|
|
601
|
-
})
|
|
2356
|
+
if (provider === "cloudflare") {
|
|
2357
|
+
const query = cloudflareCatalogQuery(requestedModel);
|
|
2358
|
+
catalogPrefix = query.prefix;
|
|
2359
|
+
directive = resolveCatalog({ target: query.target, model: query.model, effort: reasoningEffort }, true);
|
|
2360
|
+
} else if (isReasoningTarget(provider)) {
|
|
2361
|
+
directive = resolveCatalog({ target: provider, model: requestedModel, effort: reasoningEffort }, false);
|
|
2362
|
+
}
|
|
602
2363
|
}
|
|
603
2364
|
if (provider === "openrouter") {
|
|
604
2365
|
applyNestedReasoning(cloned, reasoningEffort);
|
|
605
2366
|
}
|
|
606
|
-
if (directive?.model)
|
|
2367
|
+
if (directive?.model) {
|
|
2368
|
+
cloned.model = provider === "cloudflare" ? restoreCloudflareModel(directive.model, catalogPrefix) : directive.model;
|
|
2369
|
+
}
|
|
607
2370
|
if (directive?.promptPrefix) applyPromptPrefix(cloned, directive.promptPrefix);
|
|
608
2371
|
const effectiveModel = cloned.model ?? requestedModel;
|
|
2372
|
+
const requestedEndpoint = cloudflareRequest?.endpoint ?? params.options.cloudflare?.endpoint;
|
|
2373
|
+
const cloudflareEndpoint = provider === "cloudflare" ? selectCloudflareEndpoint({
|
|
2374
|
+
model: effectiveModel,
|
|
2375
|
+
...requestedEndpoint ? { endpoint: requestedEndpoint } : {},
|
|
2376
|
+
...cloned.apiMode ? { apiMode: cloned.apiMode } : {}
|
|
2377
|
+
}) : void 0;
|
|
609
2378
|
const call = {};
|
|
610
2379
|
if (reasoningEffort !== void 0 && provider === "openrouter") {
|
|
611
2380
|
call.suppressNestedReasoningFallback = true;
|
|
612
2381
|
}
|
|
613
2382
|
const overlay = directive && hasOwnKeys(directive.body) ? directive.body : explicit && provider === "bedrock" && bedrockControls?.additionalModelRequestFields ? { additionalModelRequestFields: bedrockControls.additionalModelRequestFields } : void 0;
|
|
614
|
-
|
|
2383
|
+
const cache = cachePolicy ? resolvePromptCache({
|
|
2384
|
+
target: provider,
|
|
2385
|
+
requestedModel,
|
|
2386
|
+
effectiveModel,
|
|
2387
|
+
policy: cachePolicy,
|
|
2388
|
+
...cloudflareEndpoint ? { cloudflareEndpoint } : {}
|
|
2389
|
+
}) : void 0;
|
|
2390
|
+
const cachePlan = cache?.plan;
|
|
2391
|
+
const cacheWarnings = cache?.warnings;
|
|
2392
|
+
const cacheResolution = cache?.resolution;
|
|
2393
|
+
const cacheTouchesBody = Boolean(
|
|
2394
|
+
cachePlan && (cachePlan.stripMarkers || cachePlan.placeBreakpoint || cachePlan.sendOpenAiOptions)
|
|
2395
|
+
);
|
|
2396
|
+
if (overlay || cacheTouchesBody) {
|
|
615
2397
|
const bodyOverlay = overlay;
|
|
616
|
-
call.finalizeBody = (body) =>
|
|
2398
|
+
call.finalizeBody = (body) => {
|
|
2399
|
+
const merged = bodyOverlay ? mergeReasoningBody(body, bodyOverlay) : body;
|
|
2400
|
+
if (!cacheTouchesBody || !cachePlan) return merged;
|
|
2401
|
+
const applied = applyPromptCache(merged, cachePlan);
|
|
2402
|
+
if (cachePlan.placeBreakpoint && !applied.placed) {
|
|
2403
|
+
noteUnplacedCache(cacheResolution, cacheWarnings, provider, effectiveModel);
|
|
2404
|
+
return merged;
|
|
2405
|
+
}
|
|
2406
|
+
return applied.body;
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
if (provider === "cloudflare" && cloudflareEndpoint) {
|
|
2410
|
+
const gateway = mergeCloudflareGateway(params.options.cloudflare, cloudflareRequest);
|
|
2411
|
+
const gatewayId = cloudflareRequest?.gatewayId ?? params.options.cloudflare?.gatewayId;
|
|
2412
|
+
call.cloudflare = {
|
|
2413
|
+
endpoint: cloudflareEndpoint,
|
|
2414
|
+
...gatewayId ? { gatewayId } : {},
|
|
2415
|
+
...cloudflareRequest?.run ? { run: cloudflareRequest.run } : {},
|
|
2416
|
+
...gateway ? { gateway } : {},
|
|
2417
|
+
...cache?.plan.gateway ? { cache: cache.plan.gateway } : {}
|
|
2418
|
+
};
|
|
617
2419
|
}
|
|
618
2420
|
if (directive && hasStreamMetadata(directive)) {
|
|
619
2421
|
const response = directive.response;
|
|
@@ -634,19 +2436,51 @@ function prepareDispatchRequest(params) {
|
|
|
634
2436
|
effectiveModel,
|
|
635
2437
|
...resolution ? { resolution } : {},
|
|
636
2438
|
...directive ? { directive } : {},
|
|
2439
|
+
...cacheResolution ? { cacheResolution } : {},
|
|
2440
|
+
...cacheWarnings ? { cacheWarnings } : {},
|
|
637
2441
|
call
|
|
638
2442
|
};
|
|
639
2443
|
}
|
|
640
|
-
function
|
|
2444
|
+
function noteUnplacedCache(resolution, warnings, target, model) {
|
|
2445
|
+
if (resolution) {
|
|
2446
|
+
resolution.outcome = "ignored";
|
|
2447
|
+
delete resolution.ttl;
|
|
2448
|
+
}
|
|
2449
|
+
if (!warnings) return;
|
|
2450
|
+
const kept = warnings.filter((warning) => warning.code !== "CACHE_TTL_DEGRADED");
|
|
2451
|
+
warnings.length = 0;
|
|
2452
|
+
warnings.push(...kept, {
|
|
2453
|
+
code: "CACHE_UNSUPPORTED",
|
|
2454
|
+
message: "Explicit prompt caching needs a system or instructions prefix. The call still runs without a breakpoint.",
|
|
2455
|
+
details: { target, model }
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
function resolveCatalog(input, ignoreUnknown) {
|
|
641
2459
|
try {
|
|
642
2460
|
return resolveReasoning(input);
|
|
643
2461
|
} catch (error) {
|
|
644
2462
|
if (error instanceof AIProfilesError) {
|
|
2463
|
+
if (ignoreUnknown && (error.code === "UNKNOWN_MODEL" || error.code === "REASONING_MAPPING_MISSING")) {
|
|
2464
|
+
return {
|
|
2465
|
+
outcome: "ignored",
|
|
2466
|
+
body: {},
|
|
2467
|
+
response: {
|
|
2468
|
+
reasoningTextPaths: [],
|
|
2469
|
+
streamReasoningKeys: [],
|
|
2470
|
+
streamReasoningPaths: [],
|
|
2471
|
+
stripFromHistoryPaths: [],
|
|
2472
|
+
stripThinkTags: false
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
645
2476
|
throw new AiDispatcherError(error.code, error.message, error.details);
|
|
646
2477
|
}
|
|
647
2478
|
throw error;
|
|
648
2479
|
}
|
|
649
2480
|
}
|
|
2481
|
+
function isReasoningTarget(provider) {
|
|
2482
|
+
return provider === "openrouter" || provider === "bedrock" || provider === "openai";
|
|
2483
|
+
}
|
|
650
2484
|
function hasExplicitControl(provider, request, bedrockControls) {
|
|
651
2485
|
if (provider === "bedrock") return bedrockControls?.additionalModelRequestFields !== void 0;
|
|
652
2486
|
return request.reasoning !== void 0;
|
|
@@ -680,12 +2514,12 @@ function settleNested(policy, kind, parentEffort) {
|
|
|
680
2514
|
if (!effort) return next;
|
|
681
2515
|
const model = kind === "fusion" ? next.judgeModel : next.model;
|
|
682
2516
|
if (!model) return next;
|
|
683
|
-
const directive = resolveCatalog({ target: "openrouter", model, effort });
|
|
2517
|
+
const directive = resolveCatalog({ target: "openrouter", model, effort }, false);
|
|
684
2518
|
if (directive.model) {
|
|
685
2519
|
if (kind === "fusion") next.judgeModel = directive.model;
|
|
686
2520
|
else next.model = directive.model;
|
|
687
2521
|
}
|
|
688
|
-
if (
|
|
2522
|
+
if (isRecord6(directive.body.reasoning)) next.reasoning = directive.body.reasoning;
|
|
689
2523
|
return next;
|
|
690
2524
|
}
|
|
691
2525
|
function applyPromptPrefix(request, prefix) {
|
|
@@ -735,7 +2569,7 @@ function prefixInputItems(items, prefix) {
|
|
|
735
2569
|
}
|
|
736
2570
|
if (!Array.isArray(item.content)) continue;
|
|
737
2571
|
for (const part of item.content) {
|
|
738
|
-
if (!
|
|
2572
|
+
if (!isRecord6(part) || typeof part.text !== "string") continue;
|
|
739
2573
|
if (part.type === "text" || part.type === "input_text") {
|
|
740
2574
|
part.text = prefix + part.text;
|
|
741
2575
|
return true;
|
|
@@ -744,8 +2578,40 @@ function prefixInputItems(items, prefix) {
|
|
|
744
2578
|
}
|
|
745
2579
|
return false;
|
|
746
2580
|
}
|
|
2581
|
+
function mergeCloudflareGateway(base, over) {
|
|
2582
|
+
const gateway = {};
|
|
2583
|
+
for (const source of [base, over]) {
|
|
2584
|
+
if (!source) continue;
|
|
2585
|
+
if (source.skipCache !== void 0) gateway.skipCache = source.skipCache;
|
|
2586
|
+
if (source.cacheTtlSeconds !== void 0) gateway.cacheTtlSeconds = source.cacheTtlSeconds;
|
|
2587
|
+
if (source.cacheKey !== void 0) gateway.cacheKey = source.cacheKey;
|
|
2588
|
+
if (source.collectLog !== void 0) gateway.collectLog = source.collectLog;
|
|
2589
|
+
if (source.requestTimeoutMs !== void 0) gateway.requestTimeoutMs = source.requestTimeoutMs;
|
|
2590
|
+
if (source.maxGatewayAttempts !== void 0) gateway.maxGatewayAttempts = source.maxGatewayAttempts;
|
|
2591
|
+
if (source.retryDelayMs !== void 0) gateway.retryDelayMs = source.retryDelayMs;
|
|
2592
|
+
if (source.backoff !== void 0) gateway.backoff = source.backoff;
|
|
2593
|
+
if (source.metadata !== void 0) gateway.metadata = source.metadata;
|
|
2594
|
+
}
|
|
2595
|
+
return Object.keys(gateway).length ? gateway : void 0;
|
|
2596
|
+
}
|
|
2597
|
+
function assertCloudflareConfigured(options) {
|
|
2598
|
+
const apiToken = options.cloudflare?.apiToken ?? readEnv2("CLOUDFLARE_API_TOKEN");
|
|
2599
|
+
if (!apiToken) {
|
|
2600
|
+
throw new AiDispatcherError(
|
|
2601
|
+
"CLOUDFLARE_API_TOKEN_MISSING",
|
|
2602
|
+
"Cloudflare API token is required. Set cloudflare.apiToken or CLOUDFLARE_API_TOKEN."
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
const accountId = options.cloudflare?.accountId ?? readEnv2("CLOUDFLARE_ACCOUNT_ID");
|
|
2606
|
+
if (!accountId) {
|
|
2607
|
+
throw new AiDispatcherError(
|
|
2608
|
+
"CLOUDFLARE_ACCOUNT_ID_MISSING",
|
|
2609
|
+
"Cloudflare account id is required. Set cloudflare.accountId or CLOUDFLARE_ACCOUNT_ID."
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
747
2613
|
function assertOpenAiConfigured(options) {
|
|
748
|
-
const apiKey = options.openai?.apiKey ??
|
|
2614
|
+
const apiKey = options.openai?.apiKey ?? readEnv2("OPENAI_API_KEY");
|
|
749
2615
|
if (!apiKey) {
|
|
750
2616
|
throw new AiDispatcherError(
|
|
751
2617
|
"OPENAI_API_KEY_MISSING",
|
|
@@ -756,8 +2622,40 @@ function assertOpenAiConfigured(options) {
|
|
|
756
2622
|
function defaultModelFor(provider, options) {
|
|
757
2623
|
if (provider === "openrouter") return options.openrouter?.defaultModel;
|
|
758
2624
|
if (provider === "bedrock") return options.bedrock?.defaultModel;
|
|
2625
|
+
if (provider === "cloudflare") return options.cloudflare?.defaultModel;
|
|
759
2626
|
return options.openai?.defaultModel;
|
|
760
2627
|
}
|
|
2628
|
+
var DISPATCH_IDENTITY_FIELDS = ["orgId", "stepId", "skillId"];
|
|
2629
|
+
function absorbOverrideMetadata(request) {
|
|
2630
|
+
const overrides = request.rawOpenRouterOverrides;
|
|
2631
|
+
if (!isRecord6(overrides) || !Object.prototype.hasOwnProperty.call(overrides, "metadata")) return;
|
|
2632
|
+
if (isRecord6(overrides.metadata)) {
|
|
2633
|
+
const current = { ...request.metadata ?? {} };
|
|
2634
|
+
const extra = {};
|
|
2635
|
+
for (const [key, value] of Object.entries(overrides.metadata)) {
|
|
2636
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) extra[key] = value;
|
|
2637
|
+
}
|
|
2638
|
+
request.metadata = { ...current, ...extra };
|
|
2639
|
+
}
|
|
2640
|
+
const remaining = {};
|
|
2641
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
2642
|
+
if (key !== "metadata") remaining[key] = value;
|
|
2643
|
+
}
|
|
2644
|
+
if (Object.keys(remaining).length === 0) delete request.rawOpenRouterOverrides;
|
|
2645
|
+
else request.rawOpenRouterOverrides = remaining;
|
|
2646
|
+
}
|
|
2647
|
+
function applyDispatchIdentity(request) {
|
|
2648
|
+
const identity = {};
|
|
2649
|
+
for (const field of DISPATCH_IDENTITY_FIELDS) {
|
|
2650
|
+
const value = request[field]?.trim();
|
|
2651
|
+
if (value) identity[field] = value;
|
|
2652
|
+
delete request[field];
|
|
2653
|
+
}
|
|
2654
|
+
if (!Object.keys(identity).length) return;
|
|
2655
|
+
const rest = { ...request.metadata ?? {} };
|
|
2656
|
+
for (const key of Object.keys(identity)) delete rest[key];
|
|
2657
|
+
request.metadata = { ...identity, ...rest };
|
|
2658
|
+
}
|
|
761
2659
|
function cloneRequest(request) {
|
|
762
2660
|
const functionTools = request.functionTools?.map((tool) => ({ ...tool }));
|
|
763
2661
|
const cloned = JSON.parse(JSON.stringify(request));
|
|
@@ -770,10 +2668,10 @@ function hasOwnKeys(value) {
|
|
|
770
2668
|
function hasStreamMetadata(directive) {
|
|
771
2669
|
return directive.response.streamReasoningKeys.length > 0 || directive.response.streamReasoningPaths.length > 0;
|
|
772
2670
|
}
|
|
773
|
-
function
|
|
2671
|
+
function isRecord6(value) {
|
|
774
2672
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
775
2673
|
}
|
|
776
|
-
function
|
|
2674
|
+
function readEnv2(name) {
|
|
777
2675
|
if (typeof process === "undefined") return void 0;
|
|
778
2676
|
return process.env[name];
|
|
779
2677
|
}
|
|
@@ -838,11 +2736,12 @@ function toBedrockRequest(request) {
|
|
|
838
2736
|
...request.execution.timeoutMs !== void 0 ? { timeoutMs: request.execution.timeoutMs } : {}
|
|
839
2737
|
};
|
|
840
2738
|
}
|
|
2739
|
+
if (request.agentId !== void 0) mapped.agentId = request.agentId;
|
|
841
2740
|
if (request.metadata !== void 0) mapped.metadata = request.metadata;
|
|
842
2741
|
return mapped;
|
|
843
2742
|
}
|
|
844
2743
|
function assertBedrockSupported(request) {
|
|
845
|
-
if (request.serverTools &&
|
|
2744
|
+
if (request.serverTools && serverToolsEnabled2(request.serverTools)) {
|
|
846
2745
|
throw new AiDispatcherError(
|
|
847
2746
|
"UNSUPPORTED_REQUEST_FIELD",
|
|
848
2747
|
"Bedrock does not support OpenRouter server tools."
|
|
@@ -876,13 +2775,13 @@ function assertBedrockSupported(request) {
|
|
|
876
2775
|
function hasMessages(request) {
|
|
877
2776
|
return Array.isArray(request.messages) && request.messages.length > 0;
|
|
878
2777
|
}
|
|
879
|
-
function
|
|
2778
|
+
function serverToolsEnabled2(policy) {
|
|
880
2779
|
return Object.values(policy).some((value) => {
|
|
881
|
-
if (Array.isArray(value)) return value.some((item) =>
|
|
882
|
-
return
|
|
2780
|
+
if (Array.isArray(value)) return value.some((item) => isEnabledMode2(item));
|
|
2781
|
+
return isEnabledMode2(value);
|
|
883
2782
|
});
|
|
884
2783
|
}
|
|
885
|
-
function
|
|
2784
|
+
function isEnabledMode2(value) {
|
|
886
2785
|
return typeof value === "object" && value !== null && "mode" in value && value.mode !== "disabled";
|
|
887
2786
|
}
|
|
888
2787
|
function inputItemsToMessages(items) {
|
|
@@ -914,8 +2813,12 @@ function isBedrockToolChoice(toolChoice) {
|
|
|
914
2813
|
}
|
|
915
2814
|
|
|
916
2815
|
// src/providers/openai.ts
|
|
917
|
-
|
|
918
|
-
|
|
2816
|
+
import {
|
|
2817
|
+
OPENROUTER_METADATA_LIMITS as OPENROUTER_METADATA_LIMITS2,
|
|
2818
|
+
toProviderMetadata as toProviderMetadata2
|
|
2819
|
+
} from "@x12i/openrouter-runtime";
|
|
2820
|
+
var DEFAULT_BASE_URL2 = "https://api.openai.com/v1";
|
|
2821
|
+
var DEFAULT_TIMEOUT_MS2 = 12e4;
|
|
919
2822
|
function createOpenAiProviderAdapter(options = {}) {
|
|
920
2823
|
const resolved = resolveOpenAiOptions(options);
|
|
921
2824
|
return {
|
|
@@ -927,8 +2830,8 @@ function createOpenAiProviderAdapter(options = {}) {
|
|
|
927
2830
|
try {
|
|
928
2831
|
return await runOpenAi(request, resolved, call, requestId);
|
|
929
2832
|
} catch (error) {
|
|
930
|
-
if (error instanceof AiDispatcherError &&
|
|
931
|
-
return
|
|
2833
|
+
if (error instanceof AiDispatcherError && isConfigError2(error.code)) throw error;
|
|
2834
|
+
return failedResponse2(request, requestId, error);
|
|
932
2835
|
}
|
|
933
2836
|
},
|
|
934
2837
|
executeStreamingChat(request, call) {
|
|
@@ -937,7 +2840,7 @@ function createOpenAiProviderAdapter(options = {}) {
|
|
|
937
2840
|
};
|
|
938
2841
|
}
|
|
939
2842
|
function compileOpenAiRequest(request, options, call, streaming) {
|
|
940
|
-
|
|
2843
|
+
assertSupported2(request);
|
|
941
2844
|
const model = (request.model ?? options.defaultModel ?? "").trim();
|
|
942
2845
|
if (!model) {
|
|
943
2846
|
throw new AiDispatcherError(
|
|
@@ -951,12 +2854,12 @@ function compileOpenAiRequest(request, options, call, streaming) {
|
|
|
951
2854
|
"OpenAI API key is required. Set openai.apiKey or OPENAI_API_KEY."
|
|
952
2855
|
);
|
|
953
2856
|
}
|
|
954
|
-
const instructions =
|
|
2857
|
+
const instructions = compileInstructions2(request);
|
|
955
2858
|
const input = compileInput(request);
|
|
956
2859
|
if (input === void 0) {
|
|
957
2860
|
throw new AiDispatcherError("INPUT_REQUIRED", "Provide request.messages, request.input, or request.prompt.");
|
|
958
2861
|
}
|
|
959
|
-
let body =
|
|
2862
|
+
let body = clean2({
|
|
960
2863
|
model,
|
|
961
2864
|
input,
|
|
962
2865
|
instructions,
|
|
@@ -970,15 +2873,16 @@ function compileOpenAiRequest(request, options, call, streaming) {
|
|
|
970
2873
|
if (call?.finalizeBody) body = call.finalizeBody(body);
|
|
971
2874
|
if (streaming) body = { ...body, stream: true };
|
|
972
2875
|
else body = { ...body, stream: false };
|
|
2876
|
+
body = attachProviderMetadata(body, request);
|
|
973
2877
|
return {
|
|
974
2878
|
url: `${options.baseUrl}/responses`,
|
|
975
|
-
headers:
|
|
2879
|
+
headers: buildHeaders2(options),
|
|
976
2880
|
body,
|
|
977
2881
|
warnings: []
|
|
978
2882
|
};
|
|
979
2883
|
}
|
|
980
|
-
function
|
|
981
|
-
if (request.serverTools &&
|
|
2884
|
+
function assertSupported2(request) {
|
|
2885
|
+
if (request.serverTools && serverToolsEnabled3(request.serverTools)) {
|
|
982
2886
|
throw new AiDispatcherError(
|
|
983
2887
|
"UNSUPPORTED_REQUEST_FIELD",
|
|
984
2888
|
"Direct OpenAI does not support OpenRouter server tools."
|
|
@@ -1003,21 +2907,21 @@ function assertSupported(request) {
|
|
|
1003
2907
|
);
|
|
1004
2908
|
}
|
|
1005
2909
|
}
|
|
1006
|
-
function
|
|
2910
|
+
function serverToolsEnabled3(policy) {
|
|
1007
2911
|
return Object.values(policy).some((value) => {
|
|
1008
|
-
if (Array.isArray(value)) return value.some((item) =>
|
|
1009
|
-
return
|
|
2912
|
+
if (Array.isArray(value)) return value.some((item) => isEnabledMode3(item));
|
|
2913
|
+
return isEnabledMode3(value);
|
|
1010
2914
|
});
|
|
1011
2915
|
}
|
|
1012
|
-
function
|
|
2916
|
+
function isEnabledMode3(value) {
|
|
1013
2917
|
return typeof value === "object" && value !== null && "mode" in value && value.mode !== "disabled";
|
|
1014
2918
|
}
|
|
1015
|
-
function
|
|
2919
|
+
function compileInstructions2(request) {
|
|
1016
2920
|
if (request.instructions) return request.instructions;
|
|
1017
2921
|
if (request.system) return request.system;
|
|
1018
2922
|
const systemMessages = (request.messages ?? []).filter((message) => message.role === "system");
|
|
1019
2923
|
if (!systemMessages.length) return void 0;
|
|
1020
|
-
return systemMessages.map((message) =>
|
|
2924
|
+
return systemMessages.map((message) => contentToText2(message.content)).filter(Boolean).join("\n");
|
|
1021
2925
|
}
|
|
1022
2926
|
function compileInput(request) {
|
|
1023
2927
|
if (request.input !== void 0) return request.input;
|
|
@@ -1037,7 +2941,7 @@ function compileInputItem(message) {
|
|
|
1037
2941
|
return {
|
|
1038
2942
|
type: "function_call_output",
|
|
1039
2943
|
call_id: message.toolCallId ?? message.name ?? "tool",
|
|
1040
|
-
output:
|
|
2944
|
+
output: contentToText2(message.content)
|
|
1041
2945
|
};
|
|
1042
2946
|
}
|
|
1043
2947
|
const role = message.role === "assistant" ? "assistant" : "user";
|
|
@@ -1049,10 +2953,10 @@ function compileContent(content) {
|
|
|
1049
2953
|
if (part.type === "text") return { type: "input_text", text: part.text };
|
|
1050
2954
|
if (part.type === "image_url") return { type: "input_image", image_url: part.imageUrl };
|
|
1051
2955
|
if (part.url) {
|
|
1052
|
-
return
|
|
2956
|
+
return clean2({ type: "input_file", file_url: part.url, filename: part.fileName });
|
|
1053
2957
|
}
|
|
1054
2958
|
if (part.data) {
|
|
1055
|
-
return
|
|
2959
|
+
return clean2({ type: "input_file", file_data: part.data, filename: part.fileName });
|
|
1056
2960
|
}
|
|
1057
2961
|
throw new AiDispatcherError(
|
|
1058
2962
|
"UNSUPPORTED_REQUEST_FIELD",
|
|
@@ -1086,10 +2990,10 @@ async function runOpenAi(request, options, call, requestId) {
|
|
|
1086
2990
|
let active = compileOpenAiRequest(request, options, call, false);
|
|
1087
2991
|
let response;
|
|
1088
2992
|
for (let iteration = 0; iteration <= maxIterations; iteration += 1) {
|
|
1089
|
-
response = await
|
|
1090
|
-
const calls = functionCalls(
|
|
2993
|
+
response = await sendWithRetries2(active, options, timeoutFor2(request, options));
|
|
2994
|
+
const calls = functionCalls(isRecord7(response) ? response : {});
|
|
1091
2995
|
if (!calls.length) break;
|
|
1092
|
-
if (!calls.every((item) =>
|
|
2996
|
+
if (!calls.every((item) => resolveExecutor2(item.name, request, options))) {
|
|
1093
2997
|
return normalizeOpenAiResponse({
|
|
1094
2998
|
request,
|
|
1095
2999
|
requestId,
|
|
@@ -1107,14 +3011,14 @@ async function runOpenAi(request, options, call, requestId) {
|
|
|
1107
3011
|
results.push(await executeOpenAiTool(item, request, options));
|
|
1108
3012
|
}
|
|
1109
3013
|
functionUsage.push(...results);
|
|
1110
|
-
const
|
|
3014
|
+
const record2 = isRecord7(response) ? response : {};
|
|
1111
3015
|
active = {
|
|
1112
3016
|
...active,
|
|
1113
|
-
body:
|
|
3017
|
+
body: clean2({
|
|
1114
3018
|
...active.body,
|
|
1115
3019
|
tool_choice: "auto",
|
|
1116
|
-
previous_response_id: typeof
|
|
1117
|
-
input: results.map(
|
|
3020
|
+
previous_response_id: typeof record2.id === "string" ? record2.id : void 0,
|
|
3021
|
+
input: results.map(toFunctionCallOutput2)
|
|
1118
3022
|
})
|
|
1119
3023
|
};
|
|
1120
3024
|
}
|
|
@@ -1127,11 +3031,11 @@ async function runOpenAi(request, options, call, requestId) {
|
|
|
1127
3031
|
status: "completed"
|
|
1128
3032
|
});
|
|
1129
3033
|
}
|
|
1130
|
-
function
|
|
3034
|
+
function resolveExecutor2(name, request, options) {
|
|
1131
3035
|
return request.functionTools?.find((tool) => tool.name === name)?.executor ?? options.tools?.[name];
|
|
1132
3036
|
}
|
|
1133
3037
|
async function executeOpenAiTool(call, request, options) {
|
|
1134
|
-
const executor =
|
|
3038
|
+
const executor = resolveExecutor2(call.name, request, options);
|
|
1135
3039
|
if (!executor) {
|
|
1136
3040
|
return call;
|
|
1137
3041
|
}
|
|
@@ -1167,7 +3071,7 @@ async function executeOpenAiTool(call, request, options) {
|
|
|
1167
3071
|
return usage;
|
|
1168
3072
|
}
|
|
1169
3073
|
}
|
|
1170
|
-
function
|
|
3074
|
+
function toFunctionCallOutput2(result) {
|
|
1171
3075
|
const output = result.status === "completed" ? result.result : { error: result.error };
|
|
1172
3076
|
return {
|
|
1173
3077
|
type: "function_call_output",
|
|
@@ -1175,7 +3079,7 @@ function toFunctionCallOutput(result) {
|
|
|
1175
3079
|
output: typeof output === "string" ? output : JSON.stringify(output)
|
|
1176
3080
|
};
|
|
1177
3081
|
}
|
|
1178
|
-
function
|
|
3082
|
+
function contentToText2(content) {
|
|
1179
3083
|
if (typeof content === "string") return content;
|
|
1180
3084
|
return content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("");
|
|
1181
3085
|
}
|
|
@@ -1185,7 +3089,7 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1185
3089
|
try {
|
|
1186
3090
|
compiled = compileOpenAiRequest(request, options, call, true);
|
|
1187
3091
|
} catch (error) {
|
|
1188
|
-
yield
|
|
3092
|
+
yield errorEvent2(requestId, error);
|
|
1189
3093
|
throw error;
|
|
1190
3094
|
}
|
|
1191
3095
|
yield {
|
|
@@ -1198,7 +3102,7 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1198
3102
|
}
|
|
1199
3103
|
};
|
|
1200
3104
|
const controller = new AbortController();
|
|
1201
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
3105
|
+
const timeout = setTimeout(() => controller.abort(), timeoutFor2(request, options));
|
|
1202
3106
|
let text = "";
|
|
1203
3107
|
let reported = false;
|
|
1204
3108
|
const report = (error) => {
|
|
@@ -1216,13 +3120,13 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1216
3120
|
signal: controller.signal
|
|
1217
3121
|
});
|
|
1218
3122
|
if (!response.ok) {
|
|
1219
|
-
const failure = await
|
|
3123
|
+
const failure = await readFailure2(response);
|
|
1220
3124
|
reported = true;
|
|
1221
|
-
yield
|
|
3125
|
+
yield errorEvent2(requestId, failure);
|
|
1222
3126
|
throw failure;
|
|
1223
3127
|
}
|
|
1224
|
-
for await (const event of
|
|
1225
|
-
if (!
|
|
3128
|
+
for await (const event of parseSse2(response)) {
|
|
3129
|
+
if (!isRecord7(event)) continue;
|
|
1226
3130
|
if (event.type === "response.output_text.delta" && typeof event.delta === "string" && event.delta) {
|
|
1227
3131
|
text += event.delta;
|
|
1228
3132
|
yield { type: "stream.text.delta", requestId, data: { text: event.delta } };
|
|
@@ -1241,8 +3145,8 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1241
3145
|
argumentsDelta: event.delta
|
|
1242
3146
|
}
|
|
1243
3147
|
};
|
|
1244
|
-
} else if (event.type === "response.completed" &&
|
|
1245
|
-
const usage =
|
|
3148
|
+
} else if (event.type === "response.completed" && isRecord7(event.response)) {
|
|
3149
|
+
const usage = normalizeUsage2(event.response.usage);
|
|
1246
3150
|
if (usage) yield { type: "stream.usage", requestId, data: { usage } };
|
|
1247
3151
|
} else if (event.type === "error") {
|
|
1248
3152
|
const failure = new AiDispatcherError(
|
|
@@ -1251,7 +3155,7 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1251
3155
|
event
|
|
1252
3156
|
);
|
|
1253
3157
|
reported = true;
|
|
1254
|
-
yield
|
|
3158
|
+
yield errorEvent2(requestId, failure);
|
|
1255
3159
|
throw failure;
|
|
1256
3160
|
}
|
|
1257
3161
|
}
|
|
@@ -1265,28 +3169,28 @@ async function* streamOpenAi(request, options, call) {
|
|
|
1265
3169
|
};
|
|
1266
3170
|
} catch (error) {
|
|
1267
3171
|
const failure = error instanceof Error && error.name === "AbortError" ? new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", "OpenAI request timed out.") : report(error);
|
|
1268
|
-
if (!reported) yield
|
|
3172
|
+
if (!reported) yield errorEvent2(requestId, failure);
|
|
1269
3173
|
throw failure;
|
|
1270
3174
|
} finally {
|
|
1271
3175
|
clearTimeout(timeout);
|
|
1272
3176
|
}
|
|
1273
3177
|
}
|
|
1274
|
-
async function
|
|
3178
|
+
async function sendWithRetries2(compiled, options, timeoutMs) {
|
|
1275
3179
|
const attempts = options.maxAttempts;
|
|
1276
3180
|
let lastError;
|
|
1277
3181
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1278
3182
|
try {
|
|
1279
|
-
return await
|
|
3183
|
+
return await sendOnce2(compiled, options, timeoutMs);
|
|
1280
3184
|
} catch (error) {
|
|
1281
3185
|
lastError = error;
|
|
1282
3186
|
const retryable = error instanceof AiDispatcherError && error.code === "PROVIDER_RETRYABLE";
|
|
1283
|
-
if (!retryable || attempt >= attempts) throw
|
|
1284
|
-
await
|
|
3187
|
+
if (!retryable || attempt >= attempts) throw asTerminalError2(error);
|
|
3188
|
+
await delay2(Math.min(500 * 2 ** (attempt - 1), 5e3));
|
|
1285
3189
|
}
|
|
1286
3190
|
}
|
|
1287
3191
|
throw lastError;
|
|
1288
3192
|
}
|
|
1289
|
-
async function
|
|
3193
|
+
async function sendOnce2(compiled, options, timeoutMs) {
|
|
1290
3194
|
const controller = new AbortController();
|
|
1291
3195
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1292
3196
|
try {
|
|
@@ -1296,7 +3200,7 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
1296
3200
|
body: JSON.stringify(compiled.body),
|
|
1297
3201
|
signal: controller.signal
|
|
1298
3202
|
});
|
|
1299
|
-
if (!response.ok) throw await
|
|
3203
|
+
if (!response.ok) throw await readFailure2(response);
|
|
1300
3204
|
const text = await response.text();
|
|
1301
3205
|
return text ? JSON.parse(text) : {};
|
|
1302
3206
|
} catch (error) {
|
|
@@ -1312,7 +3216,7 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
1312
3216
|
clearTimeout(timeout);
|
|
1313
3217
|
}
|
|
1314
3218
|
}
|
|
1315
|
-
async function
|
|
3219
|
+
async function readFailure2(response) {
|
|
1316
3220
|
const text = await response.text();
|
|
1317
3221
|
let body = text;
|
|
1318
3222
|
try {
|
|
@@ -1320,21 +3224,21 @@ async function readFailure(response) {
|
|
|
1320
3224
|
} catch {
|
|
1321
3225
|
body = text;
|
|
1322
3226
|
}
|
|
1323
|
-
const message =
|
|
3227
|
+
const message = messageFrom2(body) ?? `OpenAI request failed with status ${response.status}.`;
|
|
1324
3228
|
const retryable = response.status === 429 || response.status >= 500;
|
|
1325
|
-
return new AiDispatcherError(
|
|
3229
|
+
return new AiDispatcherError(classifyStatus2(response.status), message, { status: response.status, body, retryable });
|
|
1326
3230
|
}
|
|
1327
|
-
function
|
|
3231
|
+
function classifyStatus2(status) {
|
|
1328
3232
|
if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
|
|
1329
3233
|
if (status === 404) return "PROVIDER_MODEL_NOT_FOUND";
|
|
1330
3234
|
if (status === 429) return "PROVIDER_RATE_LIMITED";
|
|
1331
3235
|
if (status === 408 || status >= 500) return "PROVIDER_RETRYABLE";
|
|
1332
3236
|
return "PROVIDER_REQUEST_FAILED";
|
|
1333
3237
|
}
|
|
1334
|
-
function
|
|
3238
|
+
function isConfigError2(code) {
|
|
1335
3239
|
return code === "OPENAI_API_KEY_MISSING" || code === "MODEL_REQUIRED" || code === "INPUT_REQUIRED" || code === "UNSUPPORTED_REQUEST_FIELD";
|
|
1336
3240
|
}
|
|
1337
|
-
function
|
|
3241
|
+
function asTerminalError2(error) {
|
|
1338
3242
|
if (error instanceof AiDispatcherError && error.code === "PROVIDER_RETRYABLE") {
|
|
1339
3243
|
return new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", error.message, error.details);
|
|
1340
3244
|
}
|
|
@@ -1342,16 +3246,16 @@ function asTerminalError(error) {
|
|
|
1342
3246
|
return new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
1343
3247
|
}
|
|
1344
3248
|
function normalizeOpenAiResponse(params) {
|
|
1345
|
-
const
|
|
1346
|
-
const text =
|
|
1347
|
-
const calls = params.functionTools ?? functionCalls(
|
|
1348
|
-
const usage =
|
|
3249
|
+
const record2 = isRecord7(params.response) ? params.response : {};
|
|
3250
|
+
const text = answerText2(record2);
|
|
3251
|
+
const calls = params.functionTools ?? functionCalls(record2);
|
|
3252
|
+
const usage = normalizeUsage2(record2.usage);
|
|
1349
3253
|
const status = params.status ?? (calls.length ? "requires_action" : "completed");
|
|
1350
3254
|
return {
|
|
1351
|
-
id: typeof
|
|
3255
|
+
id: typeof record2.id === "string" ? record2.id : params.requestId,
|
|
1352
3256
|
status,
|
|
1353
3257
|
apiMode: "responses",
|
|
1354
|
-
model: typeof
|
|
3258
|
+
model: typeof record2.model === "string" ? record2.model : modelOf2(params.compiled),
|
|
1355
3259
|
text,
|
|
1356
3260
|
citations: [],
|
|
1357
3261
|
images: [],
|
|
@@ -1364,14 +3268,14 @@ function normalizeOpenAiResponse(params) {
|
|
|
1364
3268
|
...params.request.metadata ? { metadata: params.request.metadata } : {}
|
|
1365
3269
|
};
|
|
1366
3270
|
}
|
|
1367
|
-
function
|
|
3271
|
+
function answerText2(response) {
|
|
1368
3272
|
const output = response.output;
|
|
1369
3273
|
if (Array.isArray(output)) {
|
|
1370
3274
|
const parts = [];
|
|
1371
3275
|
for (const item of output) {
|
|
1372
|
-
if (!
|
|
3276
|
+
if (!isRecord7(item) || item.type === "reasoning" || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
1373
3277
|
for (const part of item.content) {
|
|
1374
|
-
if (
|
|
3278
|
+
if (isRecord7(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
|
|
1375
3279
|
}
|
|
1376
3280
|
}
|
|
1377
3281
|
if (parts.length > 0) return parts.join("");
|
|
@@ -1382,7 +3286,7 @@ function functionCalls(response) {
|
|
|
1382
3286
|
if (!Array.isArray(response.output)) return [];
|
|
1383
3287
|
const calls = [];
|
|
1384
3288
|
for (const item of response.output) {
|
|
1385
|
-
if (!
|
|
3289
|
+
if (!isRecord7(item) || item.type !== "function_call") continue;
|
|
1386
3290
|
const name = typeof item.name === "string" ? item.name : "function";
|
|
1387
3291
|
const callId = typeof item.call_id === "string" ? item.call_id : name;
|
|
1388
3292
|
let args = item.arguments;
|
|
@@ -1413,11 +3317,11 @@ function toolUsage(calls) {
|
|
|
1413
3317
|
functionTools: calls
|
|
1414
3318
|
};
|
|
1415
3319
|
}
|
|
1416
|
-
function
|
|
1417
|
-
if (!
|
|
1418
|
-
const inputTokens =
|
|
1419
|
-
const outputTokens =
|
|
1420
|
-
const totalTokens =
|
|
3320
|
+
function normalizeUsage2(usage) {
|
|
3321
|
+
if (!isRecord7(usage)) return void 0;
|
|
3322
|
+
const inputTokens = numberValue2(usage.input_tokens);
|
|
3323
|
+
const outputTokens = numberValue2(usage.output_tokens);
|
|
3324
|
+
const totalTokens = numberValue2(usage.total_tokens) ?? (inputTokens !== void 0 || outputTokens !== void 0 ? (inputTokens ?? 0) + (outputTokens ?? 0) : void 0);
|
|
1421
3325
|
return {
|
|
1422
3326
|
raw: usage,
|
|
1423
3327
|
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
@@ -1425,7 +3329,7 @@ function normalizeUsage(usage) {
|
|
|
1425
3329
|
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1426
3330
|
};
|
|
1427
3331
|
}
|
|
1428
|
-
function
|
|
3332
|
+
function failedResponse2(request, requestId, error) {
|
|
1429
3333
|
const normalized = error instanceof AiDispatcherError ? error : new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
1430
3334
|
return {
|
|
1431
3335
|
id: requestId,
|
|
@@ -1447,7 +3351,7 @@ function failedResponse(request, requestId, error) {
|
|
|
1447
3351
|
raw: { request, response: void 0 }
|
|
1448
3352
|
};
|
|
1449
3353
|
}
|
|
1450
|
-
function
|
|
3354
|
+
function errorEvent2(requestId, error) {
|
|
1451
3355
|
const normalized = error instanceof AiDispatcherError ? error : new AiDispatcherError("PROVIDER_REQUEST_FAILED", error instanceof Error ? error.message : String(error));
|
|
1452
3356
|
return {
|
|
1453
3357
|
type: "stream.error",
|
|
@@ -1462,22 +3366,31 @@ function errorEvent(requestId, error) {
|
|
|
1462
3366
|
}
|
|
1463
3367
|
};
|
|
1464
3368
|
}
|
|
1465
|
-
function
|
|
3369
|
+
function modelOf2(compiled) {
|
|
1466
3370
|
return typeof compiled.body.model === "string" ? compiled.body.model : "";
|
|
1467
3371
|
}
|
|
1468
|
-
function
|
|
1469
|
-
if (!
|
|
3372
|
+
function messageFrom2(body) {
|
|
3373
|
+
if (!isRecord7(body)) return void 0;
|
|
1470
3374
|
if (typeof body.message === "string") return body.message;
|
|
1471
|
-
if (
|
|
3375
|
+
if (isRecord7(body.error) && typeof body.error.message === "string") return body.error.message;
|
|
1472
3376
|
return void 0;
|
|
1473
3377
|
}
|
|
1474
|
-
function
|
|
3378
|
+
function numberValue2(value) {
|
|
1475
3379
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1476
3380
|
}
|
|
1477
|
-
function
|
|
3381
|
+
function attachProviderMetadata(body, request) {
|
|
3382
|
+
const fromBody = isRecord7(body.metadata) ? body.metadata : void 0;
|
|
3383
|
+
const metadata = toProviderMetadata2(request.metadata, request.agentId, OPENROUTER_METADATA_LIMITS2, fromBody);
|
|
3384
|
+
if (metadata) return { ...body, metadata };
|
|
3385
|
+
if (body.metadata === void 0) return body;
|
|
3386
|
+
const rest = { ...body };
|
|
3387
|
+
delete rest.metadata;
|
|
3388
|
+
return rest;
|
|
3389
|
+
}
|
|
3390
|
+
function clean2(body) {
|
|
1478
3391
|
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
|
|
1479
3392
|
}
|
|
1480
|
-
function
|
|
3393
|
+
function buildHeaders2(options) {
|
|
1481
3394
|
return {
|
|
1482
3395
|
"content-type": "application/json",
|
|
1483
3396
|
authorization: `Bearer ${options.apiKey}`,
|
|
@@ -1485,13 +3398,13 @@ function buildHeaders(options) {
|
|
|
1485
3398
|
...options.project ? { "OpenAI-Project": options.project } : {}
|
|
1486
3399
|
};
|
|
1487
3400
|
}
|
|
1488
|
-
function
|
|
3401
|
+
function timeoutFor2(request, options) {
|
|
1489
3402
|
return request.execution?.timeoutMs ?? options.timeoutMs;
|
|
1490
3403
|
}
|
|
1491
|
-
function
|
|
3404
|
+
function delay2(ms) {
|
|
1492
3405
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1493
3406
|
}
|
|
1494
|
-
async function*
|
|
3407
|
+
async function* parseSse2(response) {
|
|
1495
3408
|
const body = response.body;
|
|
1496
3409
|
if (!body) throw new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", "OpenAI stream response had no body.");
|
|
1497
3410
|
const reader = body.getReader();
|
|
@@ -1504,14 +3417,14 @@ async function* parseSse(response) {
|
|
|
1504
3417
|
const parts = buffer.split("\n\n");
|
|
1505
3418
|
buffer = parts.pop() ?? "";
|
|
1506
3419
|
for (const part of parts) {
|
|
1507
|
-
const parsed =
|
|
3420
|
+
const parsed = parseSsePart2(part);
|
|
1508
3421
|
if (parsed !== void 0) yield parsed;
|
|
1509
3422
|
}
|
|
1510
3423
|
}
|
|
1511
|
-
const trailing =
|
|
3424
|
+
const trailing = parseSsePart2(buffer);
|
|
1512
3425
|
if (trailing !== void 0) yield trailing;
|
|
1513
3426
|
}
|
|
1514
|
-
function
|
|
3427
|
+
function parseSsePart2(part) {
|
|
1515
3428
|
const data = part.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
1516
3429
|
if (!data || data === "[DONE]") return void 0;
|
|
1517
3430
|
try {
|
|
@@ -1521,25 +3434,25 @@ function parseSsePart(part) {
|
|
|
1521
3434
|
}
|
|
1522
3435
|
}
|
|
1523
3436
|
function resolveOpenAiOptions(options) {
|
|
1524
|
-
const baseUrl = (options.baseUrl ??
|
|
3437
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
|
|
1525
3438
|
return {
|
|
1526
|
-
apiKey: options.apiKey ??
|
|
3439
|
+
apiKey: options.apiKey ?? readEnv3("OPENAI_API_KEY") ?? "",
|
|
1527
3440
|
baseUrl,
|
|
1528
3441
|
...options.defaultModel ? { defaultModel: options.defaultModel } : {},
|
|
1529
3442
|
...options.organization ? { organization: options.organization } : {},
|
|
1530
3443
|
...options.project ? { project: options.project } : {},
|
|
1531
|
-
timeoutMs: options.timeoutMs ??
|
|
3444
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
1532
3445
|
maxAttempts: options.maxAttempts ?? 3,
|
|
1533
3446
|
fetch: options.fetch ?? globalThis.fetch,
|
|
1534
3447
|
...options.tools ? { tools: options.tools } : {},
|
|
1535
3448
|
...options.logger ? { logger: options.logger } : {}
|
|
1536
3449
|
};
|
|
1537
3450
|
}
|
|
1538
|
-
function
|
|
3451
|
+
function readEnv3(name) {
|
|
1539
3452
|
if (typeof process === "undefined") return void 0;
|
|
1540
3453
|
return process.env[name];
|
|
1541
3454
|
}
|
|
1542
|
-
function
|
|
3455
|
+
function isRecord7(value) {
|
|
1543
3456
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1544
3457
|
}
|
|
1545
3458
|
|
|
@@ -1614,6 +3527,13 @@ function createAiDispatcher(options = {}) {
|
|
|
1614
3527
|
};
|
|
1615
3528
|
return createOpenAiProviderAdapter(openaiOptions);
|
|
1616
3529
|
}
|
|
3530
|
+
if (provider === "cloudflare") {
|
|
3531
|
+
const cloudflareOptions = {
|
|
3532
|
+
...options.cloudflare,
|
|
3533
|
+
...options.logger && !options.cloudflare?.logger ? { logger: options.logger } : {}
|
|
3534
|
+
};
|
|
3535
|
+
return createCloudflareProviderAdapter(cloudflareOptions);
|
|
3536
|
+
}
|
|
1617
3537
|
throw new AiDispatcherError(
|
|
1618
3538
|
"PROVIDER_ADAPTER_MISSING",
|
|
1619
3539
|
`No adapter registered for provider "${provider}".`
|
|
@@ -1675,22 +3595,27 @@ function createAiDispatcher(options = {}) {
|
|
|
1675
3595
|
function providerTools(provider, options) {
|
|
1676
3596
|
if (provider === "openrouter") return options.openrouter?.tools;
|
|
1677
3597
|
if (provider === "bedrock") return options.bedrock?.tools;
|
|
3598
|
+
if (provider === "cloudflare") return options.cloudflare?.tools;
|
|
1678
3599
|
return options.openai?.tools;
|
|
1679
3600
|
}
|
|
1680
3601
|
function callOf(call) {
|
|
1681
|
-
if (!call.finalizeBody && !call.extractStreamReasoning && !call.suppressNestedReasoningFallback
|
|
3602
|
+
if (!call.finalizeBody && !call.extractStreamReasoning && !call.suppressNestedReasoningFallback && !call.cloudflare) {
|
|
3603
|
+
return void 0;
|
|
3604
|
+
}
|
|
1682
3605
|
return call;
|
|
1683
3606
|
}
|
|
1684
3607
|
function attachCompileMeta(prepared, compiled) {
|
|
1685
|
-
const
|
|
1686
|
-
const headers =
|
|
3608
|
+
const record2 = compiled;
|
|
3609
|
+
const headers = isRecord8(record2.headers) ? redactHeaders(record2.headers) : void 0;
|
|
1687
3610
|
return {
|
|
1688
|
-
...
|
|
3611
|
+
...record2,
|
|
1689
3612
|
...headers ? { headers } : {},
|
|
1690
3613
|
provider: prepared.provider,
|
|
1691
3614
|
requestedModel: prepared.requestedModel,
|
|
1692
3615
|
effectiveModel: prepared.effectiveModel,
|
|
1693
|
-
...prepared.resolution ? { reasoningResolution: prepared.resolution } : {}
|
|
3616
|
+
...prepared.resolution ? { reasoningResolution: prepared.resolution } : {},
|
|
3617
|
+
...prepared.cacheResolution ? { cacheResolution: prepared.cacheResolution } : {},
|
|
3618
|
+
...prepared.cacheWarnings?.length ? { warnings: [...Array.isArray(record2.warnings) ? record2.warnings : [], ...prepared.cacheWarnings] } : {}
|
|
1694
3619
|
};
|
|
1695
3620
|
}
|
|
1696
3621
|
function redactHeaders(headers) {
|
|
@@ -1711,10 +3636,11 @@ function log(options, event, prepared, entrypoint, streaming) {
|
|
|
1711
3636
|
if (prepared.request.id) payload.requestId = prepared.request.id;
|
|
1712
3637
|
if (prepared.resolution?.outcome) payload.outcome = prepared.resolution.outcome;
|
|
1713
3638
|
if (prepared.resolution) payload.bypassed = prepared.resolution.bypassed;
|
|
3639
|
+
if (prepared.cacheResolution) payload.cacheOutcome = prepared.cacheResolution.outcome;
|
|
1714
3640
|
if (event.endsWith("compiled")) options.logger?.debug?.(event, payload);
|
|
1715
3641
|
else options.logger?.info?.(event, payload);
|
|
1716
3642
|
}
|
|
1717
|
-
function
|
|
3643
|
+
function isRecord8(value) {
|
|
1718
3644
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1719
3645
|
}
|
|
1720
3646
|
|
|
@@ -1728,6 +3654,7 @@ export {
|
|
|
1728
3654
|
AiDispatcherError,
|
|
1729
3655
|
IMPLEMENTED_AI_PROVIDERS,
|
|
1730
3656
|
createAiDispatcher,
|
|
3657
|
+
decodeProviderMetadata,
|
|
1731
3658
|
isImplementedAiProvider,
|
|
1732
3659
|
providerNotImplemented,
|
|
1733
3660
|
resolveProvider,
|