@vedmalex/ai-connect 0.8.0 → 0.10.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 +16 -4
- package/dist/browser/index.js +1030 -378
- package/dist/browser/index.js.map +3 -3
- package/dist/bun/index.js +1403 -535
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/local.js +1354 -534
- package/dist/bun/local.js.map +4 -4
- package/dist/node/index.js +1403 -535
- package/dist/node/index.js.map +4 -4
- package/dist/node/local.js +1354 -534
- package/dist/node/local.js.map +4 -4
- package/dist/types/acp.d.ts.map +1 -1
- package/dist/types/cli-presets.d.ts.map +1 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/default-handlers.d.ts.map +1 -1
- package/dist/types/errors.d.ts +11 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/fanout.d.ts.map +1 -1
- package/dist/types/files.d.ts +47 -1
- package/dist/types/files.d.ts.map +1 -1
- package/dist/types/local-handlers.d.ts.map +1 -1
- package/dist/types/logging.d.ts +18 -1
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mock-gateway.d.ts +20 -0
- package/dist/types/mock-gateway.d.ts.map +1 -1
- package/dist/types/model-reference.d.ts +5 -2
- package/dist/types/model-reference.d.ts.map +1 -1
- package/dist/types/router.d.ts.map +1 -1
- package/dist/types/server-presets.d.ts +1 -1
- package/dist/types/server-presets.d.ts.map +1 -1
- package/dist/types/server.d.ts.map +1 -1
- package/dist/types/transport-runtime.d.ts +75 -0
- package/dist/types/transport-runtime.d.ts.map +1 -0
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -12,7 +12,10 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
12
12
|
// pi takes file/image attachments as separate `@<path>` argv tokens (REQ-024);
|
|
13
13
|
// {files} expands one `@<staged-path>` token per attachment. pi stages all
|
|
14
14
|
// categories (PDFs as-is, no extraction guarantee).
|
|
15
|
-
|
|
15
|
+
// H10 (flag injection): the `--` end-of-options token precedes {files}/{prompt}
|
|
16
|
+
// — both are BARE positionals here — so an `@<path>` reference or a prompt that
|
|
17
|
+
// starts with `-` is parsed by pi as a positional, never as a flag.
|
|
18
|
+
argsTemplate: ["--print", "--model", "{model}", "--", "{files}", "{prompt}"],
|
|
16
19
|
discovery: {
|
|
17
20
|
via: "none"
|
|
18
21
|
},
|
|
@@ -32,13 +35,22 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
32
35
|
transportId: "claude-cli",
|
|
33
36
|
options: {
|
|
34
37
|
preset: "claude",
|
|
35
|
-
|
|
38
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional so a
|
|
39
|
+
// prompt starting with `-` (e.g. "--dangerously-skip-permissions") is parsed
|
|
40
|
+
// by the claude CLI as a positional, not a flag.
|
|
41
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
36
42
|
discovery: {
|
|
37
43
|
via: "acp",
|
|
38
44
|
acp: {
|
|
39
45
|
transportId: "claude-code-acp"
|
|
40
46
|
}
|
|
41
47
|
},
|
|
48
|
+
// SENTINEL (documentation-only): the `__preset__:*` textPath/errorPath values
|
|
49
|
+
// are NOT resolved as live JSON paths. This `parser` block records which
|
|
50
|
+
// dedicated parser a built-in preset maps to (parseClaudeCli); dispatch for a
|
|
51
|
+
// preset route goes through the `cliOptions.preset` switch in cli.ts
|
|
52
|
+
// (parseCliResult), which only reads a parser config when a ROUTE sets its own
|
|
53
|
+
// `cli.parser`. See the mirrored note in cli.ts::parseCliResult.
|
|
42
54
|
parser: {
|
|
43
55
|
kind: "json",
|
|
44
56
|
textPath: "__preset__:claude.result",
|
|
@@ -54,7 +66,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
54
66
|
transportId: "openclaude-cli",
|
|
55
67
|
options: {
|
|
56
68
|
preset: "openclaude",
|
|
57
|
-
|
|
69
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional (see
|
|
70
|
+
// the claude preset above for rationale).
|
|
71
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
72
|
+
// SENTINEL (documentation-only): see the claude preset note — `__preset__:*`
|
|
73
|
+
// paths are markers, not live JSON paths; dispatch is via the preset switch.
|
|
58
74
|
parser: {
|
|
59
75
|
kind: "json",
|
|
60
76
|
textPath: "__preset__:claude.result",
|
|
@@ -73,6 +89,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
73
89
|
// codex exec takes image attachments via `-i/--image <FILE>` flags
|
|
74
90
|
// (REQ-024); {files} expands a `--image <staged-path>` pair per image.
|
|
75
91
|
// Images only — a non-image attachment cleanly rejects pre-spawn.
|
|
92
|
+
// H10 (flag injection): `--` sits AFTER {files} and before the trailing
|
|
93
|
+
// {prompt} positional. Unlike pi, codex files are passed via the explicit
|
|
94
|
+
// `--image <path>` flag (the path is that flag's VALUE, not a positional), so
|
|
95
|
+
// `--` must NOT precede {files} or it would neutralize `--image`. It guards
|
|
96
|
+
// only the {prompt} positional against being parsed as a flag.
|
|
76
97
|
argsTemplate: [
|
|
77
98
|
"exec",
|
|
78
99
|
"--json",
|
|
@@ -81,6 +102,7 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
81
102
|
"--model",
|
|
82
103
|
"{model}",
|
|
83
104
|
"{files}",
|
|
105
|
+
"--",
|
|
84
106
|
"{prompt}"
|
|
85
107
|
],
|
|
86
108
|
discovery: {
|
|
@@ -94,6 +116,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
94
116
|
perFileArgs: ["--image", "{path}"],
|
|
95
117
|
categories: ["image"]
|
|
96
118
|
},
|
|
119
|
+
// SENTINEL (documentation-only): the `__preset__:*` path values below are
|
|
120
|
+
// markers naming the dedicated parser this preset maps to (parseCodexCli), NOT
|
|
121
|
+
// live JSONL selectors. Dispatch for a preset route runs through the
|
|
122
|
+
// `cliOptions.preset` switch in cli.ts::parseCliResult; this block is only ever
|
|
123
|
+
// read as a live parser when a ROUTE overrides `cli.parser`.
|
|
97
124
|
parser: {
|
|
98
125
|
kind: "jsonl",
|
|
99
126
|
text: {
|
|
@@ -130,6 +157,19 @@ var AiConnectError = class extends Error {
|
|
|
130
157
|
this.code = code;
|
|
131
158
|
this.details = details;
|
|
132
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* `Error.prototype.message` is non-enumerable, so a bare `JSON.stringify(err)` silently drops
|
|
162
|
+
* it (LOW, review-report-seed.md). Provide an explicit projection so serializing an
|
|
163
|
+
* `AiConnectError` (logs, wide events, error-response bodies) always carries the message.
|
|
164
|
+
*/
|
|
165
|
+
toJSON() {
|
|
166
|
+
return {
|
|
167
|
+
name: this.name,
|
|
168
|
+
code: this.code,
|
|
169
|
+
message: this.message,
|
|
170
|
+
details: this.details
|
|
171
|
+
};
|
|
172
|
+
}
|
|
133
173
|
};
|
|
134
174
|
function isAiConnectError(error) {
|
|
135
175
|
return error instanceof AiConnectError;
|
|
@@ -160,6 +200,16 @@ function toAiConnectError(error, fallbackCode = "temporary_unavailable") {
|
|
|
160
200
|
// src/config.ts
|
|
161
201
|
var DEFAULT_OPERATIONS = ["text", "image", "file"];
|
|
162
202
|
var DEFAULT_STRATEGY = "priority";
|
|
203
|
+
var CLI_TRANSPORT_ID_BY_PROVIDER = {
|
|
204
|
+
anthropic: "claude-cli",
|
|
205
|
+
openclaude: "openclaude-cli",
|
|
206
|
+
openai: "codex-cli",
|
|
207
|
+
pi: "pi-cli"
|
|
208
|
+
};
|
|
209
|
+
var ACP_TRANSPORT_ID_BY_PROVIDER = {
|
|
210
|
+
anthropic: "claude-code-acp",
|
|
211
|
+
openai: "codex-acp"
|
|
212
|
+
};
|
|
163
213
|
var DEFAULT_FALLBACK_ACTIONS = {
|
|
164
214
|
rate_limit: [
|
|
165
215
|
"rotate-credential",
|
|
@@ -271,33 +321,64 @@ function assert(condition, message) {
|
|
|
271
321
|
throw new AiConnectError("validation_error", message);
|
|
272
322
|
}
|
|
273
323
|
}
|
|
274
|
-
function asPositiveInteger(value, fallback) {
|
|
324
|
+
function asPositiveInteger(value, fallback, fieldLabel) {
|
|
325
|
+
if (value === void 0) {
|
|
326
|
+
return fallback;
|
|
327
|
+
}
|
|
275
328
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
329
|
+
if (fieldLabel) {
|
|
330
|
+
console.warn(
|
|
331
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
276
334
|
return fallback;
|
|
277
335
|
}
|
|
278
336
|
return Math.floor(value);
|
|
279
337
|
}
|
|
280
|
-
function asNonNegativeInteger(value, fallback) {
|
|
338
|
+
function asNonNegativeInteger(value, fallback, fieldLabel) {
|
|
339
|
+
if (value === void 0) {
|
|
340
|
+
return fallback;
|
|
341
|
+
}
|
|
281
342
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
343
|
+
if (fieldLabel) {
|
|
344
|
+
console.warn(
|
|
345
|
+
`[ai-connect] ${fieldLabel} must be a non-negative finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
282
348
|
return fallback;
|
|
283
349
|
}
|
|
284
350
|
return Math.floor(value);
|
|
285
351
|
}
|
|
286
|
-
function asOptionalContextWindow(value) {
|
|
352
|
+
function asOptionalContextWindow(value, fieldLabel) {
|
|
353
|
+
if (value === void 0) {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
287
356
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
357
|
+
if (fieldLabel) {
|
|
358
|
+
console.warn(
|
|
359
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Ignoring.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
288
362
|
return void 0;
|
|
289
363
|
}
|
|
290
364
|
return Math.floor(value);
|
|
291
365
|
}
|
|
292
|
-
function normalizeModelInput(input, accountContextWindow) {
|
|
366
|
+
function normalizeModelInput(input, accountContextWindow, accountId, providerId) {
|
|
293
367
|
if (typeof input === "string") {
|
|
294
368
|
return {
|
|
295
369
|
id: input.trim(),
|
|
296
370
|
...accountContextWindow !== void 0 ? { contextWindow: accountContextWindow } : {}
|
|
297
371
|
};
|
|
298
372
|
}
|
|
373
|
+
assert(
|
|
374
|
+
typeof input.id === "string" && input.id.trim().length > 0,
|
|
375
|
+
`Account "${accountId}" for provider "${providerId}" declared a model entry with a missing or empty "id".`
|
|
376
|
+
);
|
|
299
377
|
const id = input.id.trim();
|
|
300
|
-
const override = asOptionalContextWindow(
|
|
378
|
+
const override = asOptionalContextWindow(
|
|
379
|
+
input.contextWindow,
|
|
380
|
+
`Model "${id}" (account "${accountId}", provider "${providerId}") contextWindow`
|
|
381
|
+
);
|
|
301
382
|
const contextWindow = override ?? accountContextWindow;
|
|
302
383
|
return {
|
|
303
384
|
id,
|
|
@@ -308,7 +389,8 @@ function asFanoutBound(value) {
|
|
|
308
389
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
309
390
|
return Number.POSITIVE_INFINITY;
|
|
310
391
|
}
|
|
311
|
-
|
|
392
|
+
const floored = Math.floor(value);
|
|
393
|
+
return floored <= 0 ? Number.POSITIVE_INFINITY : floored;
|
|
312
394
|
}
|
|
313
395
|
function normalizeFanoutPolicy(policy) {
|
|
314
396
|
return {
|
|
@@ -534,7 +616,7 @@ function normalizeTransport(providerId, input) {
|
|
|
534
616
|
};
|
|
535
617
|
}
|
|
536
618
|
if (descriptor.kind === "cli") {
|
|
537
|
-
const inferredId2 = providerId
|
|
619
|
+
const inferredId2 = CLI_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "cli";
|
|
538
620
|
return {
|
|
539
621
|
kind: "cli",
|
|
540
622
|
id: descriptor.id?.trim() || inferredId2,
|
|
@@ -551,7 +633,7 @@ function normalizeTransport(providerId, input) {
|
|
|
551
633
|
...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
|
|
552
634
|
};
|
|
553
635
|
}
|
|
554
|
-
const inferredId = providerId
|
|
636
|
+
const inferredId = ACP_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "acp";
|
|
555
637
|
const normalizedLaunch = descriptor.launch ? (() => {
|
|
556
638
|
const contextMode = descriptor.launch?.contextMode ?? "workspace";
|
|
557
639
|
const skillsMode = descriptor.launch?.skillsMode ?? "default";
|
|
@@ -648,20 +730,26 @@ function defaultCapabilities(transport, runtime) {
|
|
|
648
730
|
}
|
|
649
731
|
function normalizeCredential(providerId, accountId, transport, input, index) {
|
|
650
732
|
const id = input.id?.trim() || `${accountId}-credential-${index + 1}`;
|
|
651
|
-
const
|
|
733
|
+
const trimmedApiKey = input.apiKey?.trim();
|
|
734
|
+
const trimmedApiKeyEnv = input.apiKeyEnv?.trim();
|
|
735
|
+
const source = input.source?.trim() || (trimmedApiKey ? "literal" : trimmedApiKeyEnv ? "env" : transport.kind === "api" ? "env" : "session");
|
|
652
736
|
if (transport.kind === "api") {
|
|
653
737
|
assert(
|
|
654
|
-
Boolean(
|
|
738
|
+
Boolean(trimmedApiKey) || Boolean(trimmedApiKeyEnv),
|
|
655
739
|
`API account "${accountId}" for provider "${providerId}" requires apiKey or apiKeyEnv credentials.`
|
|
656
740
|
);
|
|
657
741
|
}
|
|
658
742
|
return {
|
|
659
743
|
id,
|
|
660
744
|
source,
|
|
661
|
-
weight: asPositiveInteger(
|
|
745
|
+
weight: asPositiveInteger(
|
|
746
|
+
input.weight,
|
|
747
|
+
1,
|
|
748
|
+
`Credential "${id}" (account "${accountId}", provider "${providerId}") weight`
|
|
749
|
+
),
|
|
662
750
|
metadata: input.metadata ?? {},
|
|
663
|
-
...
|
|
664
|
-
...
|
|
751
|
+
...trimmedApiKey ? { apiKey: trimmedApiKey } : {},
|
|
752
|
+
...trimmedApiKeyEnv ? { apiKeyEnv: trimmedApiKeyEnv } : {},
|
|
665
753
|
...input.apiKeyDelimiter ? { apiKeyDelimiter: input.apiKeyDelimiter } : {}
|
|
666
754
|
};
|
|
667
755
|
}
|
|
@@ -695,6 +783,31 @@ function buildRouteId(route) {
|
|
|
695
783
|
slugify(route.model)
|
|
696
784
|
].join(":");
|
|
697
785
|
}
|
|
786
|
+
function assertRouteIdUniqueness(routes) {
|
|
787
|
+
const byId = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const route of routes) {
|
|
789
|
+
const group = byId.get(route.id);
|
|
790
|
+
if (group) {
|
|
791
|
+
group.push(route);
|
|
792
|
+
} else {
|
|
793
|
+
byId.set(route.id, [route]);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [id, group] of byId) {
|
|
797
|
+
if (group.length <= 1) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const distinctModels = [...new Set(group.map((route) => route.model))];
|
|
801
|
+
if (distinctModels.length <= 1) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const first = group[0];
|
|
805
|
+
assert(
|
|
806
|
+
false,
|
|
807
|
+
`Route id "${id}" is ambiguous: model(s) ${distinctModels.map((model) => `"${model}"`).join(", ")} on account "${first.accountId}" (provider "${first.provider}") slugify to the same route id. Rename one of the models so they no longer collide after slugification.`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
698
811
|
function routeAliases(route) {
|
|
699
812
|
return [
|
|
700
813
|
route.provider,
|
|
@@ -819,7 +932,8 @@ function defineConfig(input) {
|
|
|
819
932
|
const transport = normalizeTransport(providerId, accountInput.transport);
|
|
820
933
|
const runtime = normalizeRuntime(transport, accountInput.runtime);
|
|
821
934
|
const accountContextWindow = asOptionalContextWindow(
|
|
822
|
-
accountInput.contextWindow
|
|
935
|
+
accountInput.contextWindow,
|
|
936
|
+
`Account "${accountId}" (provider "${providerId}") contextWindow`
|
|
823
937
|
);
|
|
824
938
|
const accountModelAllowlistMode = accountInput.modelAllowlistMode ?? "strict";
|
|
825
939
|
const accountContextMode = accountInput.contextMode ?? "workspace";
|
|
@@ -829,7 +943,9 @@ function defineConfig(input) {
|
|
|
829
943
|
Array.isArray(accountInput.models) && accountInput.models.length > 0,
|
|
830
944
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
831
945
|
);
|
|
832
|
-
const models = accountInput.models.map(
|
|
946
|
+
const models = accountInput.models.map(
|
|
947
|
+
(model) => normalizeModelInput(model, accountContextWindow, accountId, providerId)
|
|
948
|
+
).filter((model) => model.id.length > 0);
|
|
833
949
|
assert(
|
|
834
950
|
models.length > 0,
|
|
835
951
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
@@ -859,8 +975,16 @@ function defineConfig(input) {
|
|
|
859
975
|
credentials,
|
|
860
976
|
capabilities,
|
|
861
977
|
verify,
|
|
862
|
-
weight: asPositiveInteger(
|
|
863
|
-
|
|
978
|
+
weight: asPositiveInteger(
|
|
979
|
+
accountInput.weight,
|
|
980
|
+
1,
|
|
981
|
+
`Account "${accountId}" (provider "${providerId}") weight`
|
|
982
|
+
),
|
|
983
|
+
priority: asNonNegativeInteger(
|
|
984
|
+
accountInput.priority,
|
|
985
|
+
accountIndex,
|
|
986
|
+
`Account "${accountId}" (provider "${providerId}") priority`
|
|
987
|
+
),
|
|
864
988
|
enabled: accountInput.enabled ?? true
|
|
865
989
|
};
|
|
866
990
|
accounts.push(normalizedAccount);
|
|
@@ -907,6 +1031,7 @@ function defineConfig(input) {
|
|
|
907
1031
|
};
|
|
908
1032
|
}
|
|
909
1033
|
assert(routes.length > 0, "At least one enabled route must be produced.");
|
|
1034
|
+
assertRouteIdUniqueness(routes);
|
|
910
1035
|
finalizeRotationCapabilities(routes);
|
|
911
1036
|
const strategy = input.routing?.strategy ?? DEFAULT_STRATEGY;
|
|
912
1037
|
const resolutionInput = input.routing?.resolution;
|
|
@@ -980,6 +1105,10 @@ function resolveRouteSelectors(selectors, routes, policy) {
|
|
|
980
1105
|
}
|
|
981
1106
|
|
|
982
1107
|
// src/fanout.ts
|
|
1108
|
+
function abortedError() {
|
|
1109
|
+
return new AiConnectError("aborted", "Operation aborted.");
|
|
1110
|
+
}
|
|
1111
|
+
var MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
983
1112
|
function createFanoutLimiter(policy, runtime) {
|
|
984
1113
|
const now = () => runtime.now?.() ?? Date.now();
|
|
985
1114
|
const { maxConcurrency, requestsPerSecond, maxCalls } = policy;
|
|
@@ -1043,10 +1172,10 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1043
1172
|
if (index >= 0) {
|
|
1044
1173
|
concurrencyQueue.splice(index, 1);
|
|
1045
1174
|
}
|
|
1046
|
-
rejectWaiter(waiter,
|
|
1175
|
+
rejectWaiter(waiter, abortedError());
|
|
1047
1176
|
};
|
|
1048
1177
|
if (signal.aborted) {
|
|
1049
|
-
reject(
|
|
1178
|
+
reject(abortedError());
|
|
1050
1179
|
return;
|
|
1051
1180
|
}
|
|
1052
1181
|
waiter.signal = signal;
|
|
@@ -1062,7 +1191,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1062
1191
|
}
|
|
1063
1192
|
while (true) {
|
|
1064
1193
|
if (signal?.aborted) {
|
|
1065
|
-
throw
|
|
1194
|
+
throw abortedError();
|
|
1066
1195
|
}
|
|
1067
1196
|
refill();
|
|
1068
1197
|
if (tokens >= 1) {
|
|
@@ -1074,6 +1203,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1074
1203
|
}
|
|
1075
1204
|
}
|
|
1076
1205
|
function delay(ms, signal) {
|
|
1206
|
+
const safeMs = Number.isFinite(ms) && ms >= 0 ? Math.min(ms, MAX_DELAY_MS) : MAX_DELAY_MS;
|
|
1077
1207
|
return new Promise((resolve, reject) => {
|
|
1078
1208
|
let cancelTimer;
|
|
1079
1209
|
const onAbort = () => {
|
|
@@ -1081,11 +1211,11 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1081
1211
|
if (signal) {
|
|
1082
1212
|
signal.removeEventListener("abort", onAbort);
|
|
1083
1213
|
}
|
|
1084
|
-
reject(
|
|
1214
|
+
reject(abortedError());
|
|
1085
1215
|
};
|
|
1086
1216
|
if (signal) {
|
|
1087
1217
|
if (signal.aborted) {
|
|
1088
|
-
reject(
|
|
1218
|
+
reject(abortedError());
|
|
1089
1219
|
return;
|
|
1090
1220
|
}
|
|
1091
1221
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1097,9 +1227,9 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1097
1227
|
resolve();
|
|
1098
1228
|
};
|
|
1099
1229
|
if (runtime.setTimer) {
|
|
1100
|
-
cancelTimer = runtime.setTimer(
|
|
1230
|
+
cancelTimer = runtime.setTimer(safeMs, done);
|
|
1101
1231
|
} else {
|
|
1102
|
-
const handle = setTimeout(done,
|
|
1232
|
+
const handle = setTimeout(done, safeMs);
|
|
1103
1233
|
cancelTimer = () => clearTimeout(handle);
|
|
1104
1234
|
}
|
|
1105
1235
|
});
|
|
@@ -1113,8 +1243,13 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1113
1243
|
);
|
|
1114
1244
|
}
|
|
1115
1245
|
callsStarted += 1;
|
|
1116
|
-
|
|
1117
|
-
|
|
1246
|
+
try {
|
|
1247
|
+
await awaitRateToken(signal);
|
|
1248
|
+
await awaitConcurrencySlot(signal);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
callsStarted -= 1;
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1118
1253
|
let released = false;
|
|
1119
1254
|
return () => {
|
|
1120
1255
|
if (released) {
|
|
@@ -1198,19 +1333,54 @@ function isRemoteUrl(value) {
|
|
|
1198
1333
|
return false;
|
|
1199
1334
|
}
|
|
1200
1335
|
}
|
|
1336
|
+
function mimeTypeAndExtensionFromMediaType(mediaTypeAndParams, fallbackMimeType, fallbackExtension) {
|
|
1337
|
+
const mimeType = mediaTypeAndParams.split(";")[0]?.trim() || fallbackMimeType;
|
|
1338
|
+
const extension2 = mimeType.split("/")[1] ?? fallbackExtension;
|
|
1339
|
+
return { mimeType, extension: extension2 };
|
|
1340
|
+
}
|
|
1201
1341
|
function parseDataUrl(value) {
|
|
1202
|
-
const
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1342
|
+
const base64Match = /^data:([^,]*?);base64,(.*)$/i.exec(value);
|
|
1343
|
+
if (base64Match) {
|
|
1344
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1345
|
+
base64Match[1] ?? "",
|
|
1346
|
+
"application/octet-stream",
|
|
1347
|
+
"bin"
|
|
1348
|
+
);
|
|
1349
|
+
return {
|
|
1350
|
+
mimeType,
|
|
1351
|
+
extension: extension2,
|
|
1352
|
+
data: base64Match[2] ?? ""
|
|
1353
|
+
};
|
|
1205
1354
|
}
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1355
|
+
const plainMatch = /^data:([^,]*?),(.*)$/i.exec(value);
|
|
1356
|
+
if (plainMatch) {
|
|
1357
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1358
|
+
plainMatch[1] ?? "",
|
|
1359
|
+
"text/plain",
|
|
1360
|
+
"txt"
|
|
1361
|
+
);
|
|
1362
|
+
let decoded;
|
|
1363
|
+
try {
|
|
1364
|
+
decoded = decodeURIComponent(plainMatch[2] ?? "");
|
|
1365
|
+
} catch {
|
|
1366
|
+
throw new AiConnectError(
|
|
1367
|
+
"validation_error",
|
|
1368
|
+
"unsupported data URL encoding"
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
mimeType,
|
|
1373
|
+
extension: extension2,
|
|
1374
|
+
data: encodeBase64(new TextEncoder().encode(decoded))
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (/^data:/i.test(value)) {
|
|
1378
|
+
throw new AiConnectError(
|
|
1379
|
+
"validation_error",
|
|
1380
|
+
"unsupported data URL encoding"
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1214
1384
|
}
|
|
1215
1385
|
function mimeTypeFromName(name) {
|
|
1216
1386
|
const ext = extension(name);
|
|
@@ -1424,9 +1594,61 @@ async function materializePortableFile(file) {
|
|
|
1424
1594
|
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1425
1595
|
}
|
|
1426
1596
|
}
|
|
1597
|
+
if (category === "other") {
|
|
1598
|
+
const base64 = await portableFileToBase64(file);
|
|
1599
|
+
if (base64 !== void 0) {
|
|
1600
|
+
payload.base64 = base64;
|
|
1601
|
+
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1602
|
+
} else if (!uri && !providerFileId) {
|
|
1603
|
+
const droppedReason = `Portable file "${file.name}" (category "other", kind "${file.kind}") has no decodable byte source and no remote uri/providerFileId; its content was dropped and this payload carries metadata only.`;
|
|
1604
|
+
payload.droppedReason = droppedReason;
|
|
1605
|
+
payload.warnings = [...payload.warnings ?? [], droppedReason];
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1427
1608
|
return payload;
|
|
1428
1609
|
}
|
|
1429
|
-
function
|
|
1610
|
+
function collapsePathSegments(value) {
|
|
1611
|
+
const normalized = value.replaceAll("\\", "/");
|
|
1612
|
+
const isAbsolute = normalized.startsWith("/");
|
|
1613
|
+
const segments = normalized.split("/");
|
|
1614
|
+
const resolved = [];
|
|
1615
|
+
for (const segment of segments) {
|
|
1616
|
+
if (segment === "" || segment === ".") {
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (segment === "..") {
|
|
1620
|
+
if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
|
|
1621
|
+
resolved.pop();
|
|
1622
|
+
} else if (!isAbsolute) {
|
|
1623
|
+
resolved.push("..");
|
|
1624
|
+
}
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
resolved.push(segment);
|
|
1628
|
+
}
|
|
1629
|
+
return (isAbsolute ? "/" : "") + resolved.join("/");
|
|
1630
|
+
}
|
|
1631
|
+
function applyPathGuard(rawPath, guard) {
|
|
1632
|
+
if (!guard) {
|
|
1633
|
+
return rawPath;
|
|
1634
|
+
}
|
|
1635
|
+
if (guard === "basename") {
|
|
1636
|
+
return basename(rawPath);
|
|
1637
|
+
}
|
|
1638
|
+
const resolved = collapsePathSegments(rawPath);
|
|
1639
|
+
const withinAllowedRoot = guard.allowedRoots.some((root) => {
|
|
1640
|
+
const resolvedRoot = collapsePathSegments(root);
|
|
1641
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}/`);
|
|
1642
|
+
});
|
|
1643
|
+
if (!withinAllowedRoot) {
|
|
1644
|
+
throw new AiConnectError(
|
|
1645
|
+
"validation_error",
|
|
1646
|
+
`Path "${rawPath}" is outside the configured allowedRoots for portable file attachments.`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return rawPath;
|
|
1650
|
+
}
|
|
1651
|
+
function preparePortableFile(input, options) {
|
|
1430
1652
|
if (isPortableFile(input)) {
|
|
1431
1653
|
return input;
|
|
1432
1654
|
}
|
|
@@ -1452,11 +1674,12 @@ function preparePortableFile(input) {
|
|
|
1452
1674
|
}
|
|
1453
1675
|
};
|
|
1454
1676
|
}
|
|
1677
|
+
const guardedPath = applyPathGuard(input, options?.pathGuard);
|
|
1455
1678
|
return {
|
|
1456
|
-
id: `path:${
|
|
1679
|
+
id: `path:${guardedPath}`,
|
|
1457
1680
|
kind: "path",
|
|
1458
|
-
name: basename(
|
|
1459
|
-
source:
|
|
1681
|
+
name: basename(guardedPath),
|
|
1682
|
+
source: guardedPath
|
|
1460
1683
|
};
|
|
1461
1684
|
}
|
|
1462
1685
|
if (isRemoteReference(input)) {
|
|
@@ -1497,17 +1720,76 @@ function preparePortableFile(input) {
|
|
|
1497
1720
|
}
|
|
1498
1721
|
|
|
1499
1722
|
// src/logging.ts
|
|
1723
|
+
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
1724
|
+
"apikey",
|
|
1725
|
+
"authorization",
|
|
1726
|
+
"password",
|
|
1727
|
+
"token",
|
|
1728
|
+
"x-api-key",
|
|
1729
|
+
"x-goog-api-key"
|
|
1730
|
+
]);
|
|
1731
|
+
var REDACTED = "[REDACTED]";
|
|
1732
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1733
|
+
var SECRET_PREFIX_RE = /^(sk-|sk-ant-|xox[baprs]-|ghp_|gho_|AIza)/;
|
|
1734
|
+
function looksLikeSecretValue(value) {
|
|
1735
|
+
if (UUID_RE.test(value)) {
|
|
1736
|
+
return false;
|
|
1737
|
+
}
|
|
1738
|
+
if (/^Bearer\s+/i.test(value)) {
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
if (SECRET_PREFIX_RE.test(value)) {
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
return /^[A-Za-z0-9+/=_]{40,}$/.test(value);
|
|
1745
|
+
}
|
|
1746
|
+
function redactNode(node) {
|
|
1747
|
+
if (node === null || node === void 0) {
|
|
1748
|
+
return node;
|
|
1749
|
+
}
|
|
1750
|
+
if (typeof node === "string") {
|
|
1751
|
+
return looksLikeSecretValue(node) ? REDACTED : node;
|
|
1752
|
+
}
|
|
1753
|
+
if (typeof node !== "object") {
|
|
1754
|
+
return node;
|
|
1755
|
+
}
|
|
1756
|
+
if (Array.isArray(node)) {
|
|
1757
|
+
return node.map((item) => redactNode(item));
|
|
1758
|
+
}
|
|
1759
|
+
const result = {};
|
|
1760
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1761
|
+
result[key] = REDACT_KEYS.has(key.toLowerCase()) ? REDACTED : redactNode(value);
|
|
1762
|
+
}
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1765
|
+
function redactEvent(event) {
|
|
1766
|
+
return redactNode(event);
|
|
1767
|
+
}
|
|
1768
|
+
function redactBaseUrl(url) {
|
|
1769
|
+
try {
|
|
1770
|
+
const parsed = new URL(url);
|
|
1771
|
+
parsed.username = "";
|
|
1772
|
+
parsed.password = "";
|
|
1773
|
+
parsed.search = "";
|
|
1774
|
+
parsed.hash = "";
|
|
1775
|
+
return parsed.toString();
|
|
1776
|
+
} catch {
|
|
1777
|
+
const withoutHash = url.split("#")[0] ?? url;
|
|
1778
|
+
return withoutHash.split("?")[0] ?? withoutHash;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1500
1781
|
function createConsoleWideEventLogger(options = {}) {
|
|
1501
1782
|
const write = options.write ?? ((line) => {
|
|
1502
1783
|
console.info(line);
|
|
1503
1784
|
});
|
|
1504
1785
|
return (event) => {
|
|
1786
|
+
const redacted = redactEvent(event);
|
|
1505
1787
|
write(
|
|
1506
|
-
options.pretty ? JSON.stringify(
|
|
1788
|
+
options.pretty ? JSON.stringify(redacted, null, 2) : JSON.stringify(redacted)
|
|
1507
1789
|
);
|
|
1508
1790
|
};
|
|
1509
1791
|
}
|
|
1510
|
-
async function shouldEmitWideEvent(event, options = {}) {
|
|
1792
|
+
async function shouldEmitWideEvent(event, options = {}, random = Math.random) {
|
|
1511
1793
|
const sampleRate = options.sampleRate ?? 1;
|
|
1512
1794
|
if ((options.keepErrors ?? true) && event.outcome === "error") {
|
|
1513
1795
|
return true;
|
|
@@ -1541,7 +1823,7 @@ async function shouldEmitWideEvent(event, options = {}) {
|
|
|
1541
1823
|
if (sampleRate >= 1) {
|
|
1542
1824
|
return true;
|
|
1543
1825
|
}
|
|
1544
|
-
return
|
|
1826
|
+
return random() < sampleRate;
|
|
1545
1827
|
}
|
|
1546
1828
|
|
|
1547
1829
|
// src/router.ts
|
|
@@ -1606,6 +1888,19 @@ function buildWeightedOrder(routes) {
|
|
|
1606
1888
|
}
|
|
1607
1889
|
return expanded;
|
|
1608
1890
|
}
|
|
1891
|
+
var ROUTING_STRATEGIES = {
|
|
1892
|
+
priority: (ordered) => ordered,
|
|
1893
|
+
failover: (ordered) => ordered,
|
|
1894
|
+
"round-robin": (ordered, cursor, cursorKey) => {
|
|
1895
|
+
const index = cursor(cursorKey, ordered.length);
|
|
1896
|
+
return rotate(ordered, index);
|
|
1897
|
+
},
|
|
1898
|
+
"weighted-round-robin": (ordered, cursor, cursorKey) => {
|
|
1899
|
+
const weighted = buildWeightedOrder(ordered);
|
|
1900
|
+
const index = cursor(cursorKey, weighted.length);
|
|
1901
|
+
return unique(rotate(weighted, index));
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1609
1904
|
var RouteRegistry = class {
|
|
1610
1905
|
config;
|
|
1611
1906
|
runtime;
|
|
@@ -1676,14 +1971,21 @@ var RouteRegistry = class {
|
|
|
1676
1971
|
const routeIds = request.routeHints?.pool?.length ? resolveRouteSelectors(request.routeHints.pool, this.config.routes) : operationRouting.pool;
|
|
1677
1972
|
const excludedIds = new Set(request.routeHints?.excludeRouteIds ?? []);
|
|
1678
1973
|
const requestedModel = request.routeHints?.model;
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1974
|
+
const eligible = routeIds.map((routeId) => this.#routeMap.get(routeId)).filter((route) => Boolean(route)).filter((route) => !excludedIds.has(route.id)).filter((route) => runtimeEligible(route, this.runtime)).filter((route) => capabilityMatch(route, request.routeHints)).map((route) => applyRequestedModel(route, requestedModel)).filter((route) => Boolean(route));
|
|
1975
|
+
const strategy = request.routeHints?.strategy ?? operationRouting.strategy;
|
|
1976
|
+
const cursorKey = `${operation}:${strategy}:${eligible.map((route) => route.id).join("|")}`;
|
|
1977
|
+
const healthyIds = new Set(
|
|
1978
|
+
eligible.filter((route) => {
|
|
1979
|
+
const state = this.getHealth(route).state;
|
|
1980
|
+
return state !== "cooling_down" && state !== "unavailable";
|
|
1981
|
+
}).map((route) => route.id)
|
|
1982
|
+
);
|
|
1683
1983
|
return this.#order(
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1984
|
+
strategy,
|
|
1985
|
+
eligible,
|
|
1986
|
+
healthyIds,
|
|
1987
|
+
request.routeHints?.preferredProviders,
|
|
1988
|
+
cursorKey
|
|
1687
1989
|
);
|
|
1688
1990
|
}
|
|
1689
1991
|
recordSuccess(route) {
|
|
@@ -1716,20 +2018,12 @@ var RouteRegistry = class {
|
|
|
1716
2018
|
failureCount
|
|
1717
2019
|
});
|
|
1718
2020
|
}
|
|
1719
|
-
#order(strategy,
|
|
1720
|
-
const sorted = baseSort(
|
|
1721
|
-
const
|
|
1722
|
-
const ordered =
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1726
|
-
if (strategy === "round-robin") {
|
|
1727
|
-
const index2 = this.#cursor(cursorKey, ordered.length);
|
|
1728
|
-
return rotate(ordered, index2);
|
|
1729
|
-
}
|
|
1730
|
-
const weighted = buildWeightedOrder(ordered);
|
|
1731
|
-
const index = this.#cursor(cursorKey, weighted.length);
|
|
1732
|
-
return unique(rotate(weighted, index));
|
|
2021
|
+
#order(strategy, eligible, healthyIds, preferredProviders, cursorKey) {
|
|
2022
|
+
const sorted = baseSort(eligible, preferredProviders);
|
|
2023
|
+
const orderedFull = this.config.routing.shuffleOnInit ? this.#shuffle(cursorKey, sorted) : sorted;
|
|
2024
|
+
const ordered = orderedFull.filter((route) => healthyIds.has(route.id));
|
|
2025
|
+
const strategyFn = ROUTING_STRATEGIES[strategy];
|
|
2026
|
+
return strategyFn(ordered, (key, length) => this.#cursor(key, length), cursorKey);
|
|
1733
2027
|
}
|
|
1734
2028
|
#shuffle(key, routes) {
|
|
1735
2029
|
const cached = this.#shuffles.get(key);
|
|
@@ -1833,8 +2127,22 @@ var MODEL_REFERENCE = {
|
|
|
1833
2127
|
"gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
|
|
1834
2128
|
"gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
|
|
1835
2129
|
};
|
|
2130
|
+
var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2131
|
+
"minimal",
|
|
2132
|
+
"low",
|
|
2133
|
+
"medium",
|
|
2134
|
+
"high",
|
|
2135
|
+
"xhigh"
|
|
2136
|
+
]);
|
|
1836
2137
|
function normalizeModelKey(model) {
|
|
1837
2138
|
let key = model.trim().toLowerCase();
|
|
2139
|
+
const lastSlashIndex = key.lastIndexOf("/");
|
|
2140
|
+
if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
|
|
2141
|
+
const tail = key.slice(lastSlashIndex + 1);
|
|
2142
|
+
if (REASONING_EFFORT_SUFFIXES.has(tail)) {
|
|
2143
|
+
key = key.slice(0, lastSlashIndex);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
1838
2146
|
const slashIndex = key.indexOf("/");
|
|
1839
2147
|
if (slashIndex > 0) {
|
|
1840
2148
|
key = key.slice(slashIndex + 1);
|
|
@@ -1842,6 +2150,7 @@ function normalizeModelKey(model) {
|
|
|
1842
2150
|
key = key.replace(/:(?:free|beta)$/i, "");
|
|
1843
2151
|
return key.trim();
|
|
1844
2152
|
}
|
|
2153
|
+
var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
|
|
1845
2154
|
function lookupModelRef(model) {
|
|
1846
2155
|
const normalized = normalizeModelKey(model);
|
|
1847
2156
|
if (!normalized) {
|
|
@@ -1864,7 +2173,7 @@ function lookupModelRef(model) {
|
|
|
1864
2173
|
}
|
|
1865
2174
|
let substringWinner;
|
|
1866
2175
|
for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
|
|
1867
|
-
if (normalized.includes(key)) {
|
|
2176
|
+
if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
|
|
1868
2177
|
if (!substringWinner || key.length > substringWinner.key.length) {
|
|
1869
2178
|
substringWinner = entry;
|
|
1870
2179
|
}
|
|
@@ -2096,6 +2405,14 @@ var DEFAULT_LOGGING_SERVICE = "ai-connect";
|
|
|
2096
2405
|
function splitCredentialValues(value, delimiter) {
|
|
2097
2406
|
return value.split(delimiter).map((item) => item.trim()).filter(Boolean);
|
|
2098
2407
|
}
|
|
2408
|
+
var ClientToolExecutionError = class extends Error {
|
|
2409
|
+
aiError;
|
|
2410
|
+
constructor(aiError) {
|
|
2411
|
+
super(aiError.message);
|
|
2412
|
+
this.name = "ClientToolExecutionError";
|
|
2413
|
+
this.aiError = aiError;
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2099
2416
|
function materializeRuntimeConfig(config, runtime) {
|
|
2100
2417
|
const routeExpansion = /* @__PURE__ */ new Map();
|
|
2101
2418
|
const routes = [];
|
|
@@ -2110,13 +2427,14 @@ function materializeRuntimeConfig(config, runtime) {
|
|
|
2110
2427
|
continue;
|
|
2111
2428
|
}
|
|
2112
2429
|
const expandedIds = [];
|
|
2430
|
+
const expandedWeight = Math.max(1, Math.floor(route.weight / values.length));
|
|
2113
2431
|
values.forEach((apiKey, index) => {
|
|
2114
2432
|
const credentialId = `${route.credentialId}#${index + 1}`;
|
|
2115
2433
|
const expandedRoute = {
|
|
2116
2434
|
...route,
|
|
2117
2435
|
id: `${route.id}#${index + 1}`,
|
|
2118
2436
|
credentialId,
|
|
2119
|
-
weight:
|
|
2437
|
+
weight: expandedWeight,
|
|
2120
2438
|
credential: {
|
|
2121
2439
|
...route.credential,
|
|
2122
2440
|
id: credentialId,
|
|
@@ -2157,6 +2475,27 @@ async function sleep(delayMs) {
|
|
|
2157
2475
|
}
|
|
2158
2476
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2159
2477
|
}
|
|
2478
|
+
async function runWithAbortRace(handlerPromise, abort) {
|
|
2479
|
+
if (abort.signal.aborted) {
|
|
2480
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2481
|
+
});
|
|
2482
|
+
throw mapAbortError(abort.reason);
|
|
2483
|
+
}
|
|
2484
|
+
let onAbort;
|
|
2485
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2486
|
+
onAbort = () => reject(mapAbortError(abort.reason));
|
|
2487
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2488
|
+
});
|
|
2489
|
+
try {
|
|
2490
|
+
return await Promise.race([handlerPromise, abortPromise]);
|
|
2491
|
+
} finally {
|
|
2492
|
+
if (onAbort) {
|
|
2493
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
2494
|
+
}
|
|
2495
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2160
2499
|
function normalizeWorkingDirectory(value) {
|
|
2161
2500
|
if (value === void 0) {
|
|
2162
2501
|
return void 0;
|
|
@@ -2198,6 +2537,12 @@ function normalizeClientToolRegistry(tools) {
|
|
|
2198
2537
|
const registry = /* @__PURE__ */ new Map();
|
|
2199
2538
|
for (const tool of tools ?? []) {
|
|
2200
2539
|
const normalized = normalizeClientToolDefinition(tool);
|
|
2540
|
+
if (registry.has(normalized.function.name)) {
|
|
2541
|
+
throw new AiConnectError(
|
|
2542
|
+
"validation_error",
|
|
2543
|
+
`Duplicate client tool name "${normalized.function.name}" registered on this client.`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2201
2546
|
registry.set(normalized.function.name, normalized);
|
|
2202
2547
|
}
|
|
2203
2548
|
return registry;
|
|
@@ -2223,10 +2568,22 @@ function resolveClientToolSelections(selections, registry) {
|
|
|
2223
2568
|
`No client tool named "${name}" is registered on this client.`
|
|
2224
2569
|
);
|
|
2225
2570
|
}
|
|
2571
|
+
if (resolved.has(name)) {
|
|
2572
|
+
throw new AiConnectError(
|
|
2573
|
+
"validation_error",
|
|
2574
|
+
`Duplicate client tool "${name}" in the clientTools selection.`
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2226
2577
|
resolved.set(name, registered);
|
|
2227
2578
|
continue;
|
|
2228
2579
|
}
|
|
2229
2580
|
const normalized = normalizeClientToolDefinition(selection);
|
|
2581
|
+
if (resolved.has(normalized.function.name)) {
|
|
2582
|
+
throw new AiConnectError(
|
|
2583
|
+
"validation_error",
|
|
2584
|
+
`Duplicate client tool "${normalized.function.name}" in the clientTools selection.`
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2230
2587
|
resolved.set(normalized.function.name, normalized);
|
|
2231
2588
|
}
|
|
2232
2589
|
return Array.from(resolved.values());
|
|
@@ -2265,6 +2622,73 @@ function normalizeToolResult(result) {
|
|
|
2265
2622
|
...result.isError !== void 0 ? { isError: result.isError } : {}
|
|
2266
2623
|
};
|
|
2267
2624
|
}
|
|
2625
|
+
var MAX_CLIENT_TOOL_LOOP_DEPTH = 8;
|
|
2626
|
+
async function* executeToolCallsRound(params) {
|
|
2627
|
+
const { toolCalls, registry, route, request, runtime, messages, abort } = params;
|
|
2628
|
+
const assistantMessage = {
|
|
2629
|
+
role: "assistant",
|
|
2630
|
+
content: params.assistantText,
|
|
2631
|
+
toolCalls
|
|
2632
|
+
};
|
|
2633
|
+
const toolMessages = [];
|
|
2634
|
+
for (const toolCall of toolCalls) {
|
|
2635
|
+
const tool = registry.get(toolCall.name);
|
|
2636
|
+
if (!tool) {
|
|
2637
|
+
throw new ClientToolExecutionError(
|
|
2638
|
+
new AiConnectError(
|
|
2639
|
+
"validation_error",
|
|
2640
|
+
`No client tool handler is registered for "${toolCall.name}".`
|
|
2641
|
+
)
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
if (abort.signal.aborted) {
|
|
2645
|
+
throw mapAbortError(abort.reason);
|
|
2646
|
+
}
|
|
2647
|
+
const startEvent = params.onToolStart?.(toolCall);
|
|
2648
|
+
if (startEvent) {
|
|
2649
|
+
yield startEvent;
|
|
2650
|
+
}
|
|
2651
|
+
let execution;
|
|
2652
|
+
try {
|
|
2653
|
+
execution = normalizeToolResult(
|
|
2654
|
+
await tool.execute(toolCall.arguments, {
|
|
2655
|
+
route,
|
|
2656
|
+
request,
|
|
2657
|
+
runtime,
|
|
2658
|
+
toolCall,
|
|
2659
|
+
messages,
|
|
2660
|
+
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
2661
|
+
})
|
|
2662
|
+
);
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
if (abort.signal.aborted) {
|
|
2665
|
+
throw mapAbortError(abort.reason);
|
|
2666
|
+
}
|
|
2667
|
+
throw new ClientToolExecutionError(
|
|
2668
|
+
new AiConnectError(
|
|
2669
|
+
"validation_error",
|
|
2670
|
+
`Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
2671
|
+
{ cause: error }
|
|
2672
|
+
)
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
toolMessages.push({
|
|
2676
|
+
role: "tool",
|
|
2677
|
+
content: execution.content,
|
|
2678
|
+
toolName: tool.function.name,
|
|
2679
|
+
toolCallId: toolCall.id,
|
|
2680
|
+
...execution.data !== void 0 ? { data: execution.data } : {},
|
|
2681
|
+
...execution.isError !== void 0 ? { isError: execution.isError } : {}
|
|
2682
|
+
});
|
|
2683
|
+
const finishEvent = params.onToolFinish?.(toolCall, {
|
|
2684
|
+
...execution.isError !== void 0 ? { isError: execution.isError } : {}
|
|
2685
|
+
});
|
|
2686
|
+
if (finishEvent) {
|
|
2687
|
+
yield finishEvent;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return { assistantMessage, toolMessages };
|
|
2691
|
+
}
|
|
2268
2692
|
function sumOptionalNumbers(left, right) {
|
|
2269
2693
|
if (left === void 0) {
|
|
2270
2694
|
return right;
|
|
@@ -2339,6 +2763,34 @@ function requiresImageInput(request) {
|
|
|
2339
2763
|
function requiresFileUpload(request) {
|
|
2340
2764
|
return request.attachments.some((file) => isPortableDocumentFile(file));
|
|
2341
2765
|
}
|
|
2766
|
+
function resolveRequiredCapabilities(request) {
|
|
2767
|
+
const requiresSchema = requiresToolSchema(request);
|
|
2768
|
+
const requiresClientTools = requiresClientToolExecution(request);
|
|
2769
|
+
const requiresImage = requiresImageInput(request);
|
|
2770
|
+
const requiresFile = requiresFileUpload(request);
|
|
2771
|
+
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2772
|
+
const projected = requiresAnyCapability ? {
|
|
2773
|
+
...request,
|
|
2774
|
+
routeHints: {
|
|
2775
|
+
...request.routeHints ?? {},
|
|
2776
|
+
requiredCapabilities: {
|
|
2777
|
+
...request.routeHints?.requiredCapabilities ?? {},
|
|
2778
|
+
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2779
|
+
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2780
|
+
...requiresImage ? { supportsImageInput: true } : {},
|
|
2781
|
+
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
} : request;
|
|
2785
|
+
return {
|
|
2786
|
+
requiresSchema,
|
|
2787
|
+
requiresClientTools,
|
|
2788
|
+
requiresImage,
|
|
2789
|
+
requiresFile,
|
|
2790
|
+
requiresAnyCapability,
|
|
2791
|
+
request: projected
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2342
2794
|
function normalizeRequestBase(request) {
|
|
2343
2795
|
if (request.messages.length === 0) {
|
|
2344
2796
|
throw new AiConnectError("validation_error", "Unified generate/stream requests must include at least one message.");
|
|
@@ -2547,7 +2999,10 @@ function summarizeRoute(route) {
|
|
|
2547
2999
|
model: route.model,
|
|
2548
3000
|
handlerKey: route.handlerKey,
|
|
2549
3001
|
profileId: route.profileId,
|
|
2550
|
-
|
|
3002
|
+
// SYS-3 / C1a: never emit a raw baseUrl into a wide event — a token embedded in
|
|
3003
|
+
// userinfo (`https://user:secret@host`) or the query string would leak verbatim,
|
|
3004
|
+
// unlike the secret-safe toPublicRoute projection. Strip credentials first.
|
|
3005
|
+
...route.transport.baseUrl ? { baseUrl: redactBaseUrl(route.transport.baseUrl) } : {}
|
|
2551
3006
|
};
|
|
2552
3007
|
}
|
|
2553
3008
|
function summarizeFiles(files) {
|
|
@@ -2837,7 +3292,7 @@ function createClient(configOrInput, options = {}) {
|
|
|
2837
3292
|
let output = initialOutput;
|
|
2838
3293
|
let messages = [...request.messages];
|
|
2839
3294
|
let transportSummary2;
|
|
2840
|
-
for (let round = 0;
|
|
3295
|
+
for (let round = 0; ; round += 1) {
|
|
2841
3296
|
const toolCalls = output.toolCalls ?? [];
|
|
2842
3297
|
if (toolCalls.length === 0) {
|
|
2843
3298
|
return {
|
|
@@ -2849,66 +3304,58 @@ function createClient(configOrInput, options = {}) {
|
|
|
2849
3304
|
...transportSummary2 ? { transportSummary: transportSummary2 } : {}
|
|
2850
3305
|
};
|
|
2851
3306
|
}
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
}
|
|
2866
|
-
const execution = normalizeToolResult(
|
|
2867
|
-
await tool.execute(toolCall.arguments, {
|
|
2868
|
-
route,
|
|
2869
|
-
request,
|
|
2870
|
-
runtime,
|
|
2871
|
-
toolCall,
|
|
2872
|
-
messages,
|
|
2873
|
-
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
2874
|
-
})
|
|
2875
|
-
);
|
|
2876
|
-
toolMessages.push({
|
|
2877
|
-
role: "tool",
|
|
2878
|
-
content: execution.content,
|
|
2879
|
-
toolName: tool.function.name,
|
|
2880
|
-
toolCallId: toolCall.id,
|
|
2881
|
-
...execution.data !== void 0 ? { data: execution.data } : {},
|
|
2882
|
-
...execution.isError !== void 0 ? { isError: execution.isError } : {}
|
|
2883
|
-
});
|
|
3307
|
+
const roundIterator = executeToolCallsRound({
|
|
3308
|
+
toolCalls,
|
|
3309
|
+
assistantText: output.text ?? "",
|
|
3310
|
+
registry,
|
|
3311
|
+
route,
|
|
3312
|
+
request,
|
|
3313
|
+
runtime,
|
|
3314
|
+
messages,
|
|
3315
|
+
abort
|
|
3316
|
+
});
|
|
3317
|
+
let roundStep = await roundIterator.next();
|
|
3318
|
+
while (!roundStep.done) {
|
|
3319
|
+
roundStep = await roundIterator.next();
|
|
2884
3320
|
}
|
|
2885
|
-
messages = [
|
|
3321
|
+
messages = [
|
|
3322
|
+
...messages,
|
|
3323
|
+
roundStep.value.assistantMessage,
|
|
3324
|
+
...roundStep.value.toolMessages
|
|
3325
|
+
];
|
|
2886
3326
|
if (abort.signal.aborted) {
|
|
2887
3327
|
throw mapAbortError(abort.reason);
|
|
2888
3328
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3329
|
+
if (round + 1 >= MAX_CLIENT_TOOL_LOOP_DEPTH) {
|
|
3330
|
+
throw new ClientToolExecutionError(
|
|
3331
|
+
new AiConnectError(
|
|
3332
|
+
"validation_error",
|
|
3333
|
+
`clientTools exceeded the maximum tool-call loop depth of ${MAX_CLIENT_TOOL_LOOP_DEPTH}.`
|
|
3334
|
+
)
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
output = await runWithAbortRace(
|
|
3338
|
+
handler({
|
|
3339
|
+
route,
|
|
3340
|
+
request: {
|
|
3341
|
+
...request,
|
|
3342
|
+
messages,
|
|
3343
|
+
attachments: []
|
|
3344
|
+
},
|
|
3345
|
+
runtime,
|
|
3346
|
+
attempt: baseAttempt,
|
|
3347
|
+
abort,
|
|
3348
|
+
telemetry: {
|
|
3349
|
+
captureTransport(summary) {
|
|
3350
|
+
transportSummary2 = summary;
|
|
3351
|
+
}
|
|
2902
3352
|
}
|
|
2903
|
-
}
|
|
2904
|
-
|
|
3353
|
+
}),
|
|
3354
|
+
abort
|
|
3355
|
+
);
|
|
2905
3356
|
warnings.push(...output.warnings ?? []);
|
|
2906
3357
|
usage = mergeUsage(usage, output.usage);
|
|
2907
3358
|
}
|
|
2908
|
-
throw new AiConnectError(
|
|
2909
|
-
"validation_error",
|
|
2910
|
-
"clientTools exceeded the maximum tool-call loop depth of 8."
|
|
2911
|
-
);
|
|
2912
3359
|
}
|
|
2913
3360
|
async function emitWideEvent(event) {
|
|
2914
3361
|
if (!logging?.logger) {
|
|
@@ -2938,26 +3385,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
2938
3385
|
async function executeGenerateOperation(normalizedRequest, operationName, abort, seed = {}) {
|
|
2939
3386
|
const startedAt = seed.startMs ?? nowMs(runtime);
|
|
2940
3387
|
const requestId = seed.requestId ?? resolveEventRequestId(normalizedRequest.logContext);
|
|
2941
|
-
const
|
|
2942
|
-
const
|
|
2943
|
-
const requiresImage = requiresImageInput(normalizedRequest);
|
|
2944
|
-
const requiresFile = requiresFileUpload(normalizedRequest);
|
|
2945
|
-
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2946
|
-
const resolvedCandidates = router.resolveCandidates(
|
|
2947
|
-
requiresAnyCapability ? {
|
|
2948
|
-
...normalizedRequest,
|
|
2949
|
-
routeHints: {
|
|
2950
|
-
...normalizedRequest.routeHints ?? {},
|
|
2951
|
-
requiredCapabilities: {
|
|
2952
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
2953
|
-
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2954
|
-
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2955
|
-
...requiresImage ? { supportsImageInput: true } : {},
|
|
2956
|
-
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
} : normalizedRequest
|
|
2960
|
-
);
|
|
3388
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
3389
|
+
const resolvedCandidates = router.resolveCandidates(capabilityScopedRequest);
|
|
2961
3390
|
const candidates = seed.candidates ?? resolvedCandidates.map(summarizeRoute);
|
|
2962
3391
|
const requestSummary = summarizeRequest(normalizedRequest, candidates);
|
|
2963
3392
|
if (resolvedCandidates.length === 0) {
|
|
@@ -3045,18 +3474,21 @@ function createClient(configOrInput, options = {}) {
|
|
|
3045
3474
|
const attemptStartedAt = nowMs(runtime);
|
|
3046
3475
|
let transportSummary2;
|
|
3047
3476
|
try {
|
|
3048
|
-
let output = await
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3477
|
+
let output = await runWithAbortRace(
|
|
3478
|
+
handler.generate({
|
|
3479
|
+
route,
|
|
3480
|
+
request: routeRequest,
|
|
3481
|
+
runtime,
|
|
3482
|
+
attempt: attempts.length + 1,
|
|
3483
|
+
abort,
|
|
3484
|
+
telemetry: {
|
|
3485
|
+
captureTransport(summary) {
|
|
3486
|
+
transportSummary2 = summary;
|
|
3487
|
+
}
|
|
3057
3488
|
}
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3489
|
+
}),
|
|
3490
|
+
abort
|
|
3491
|
+
);
|
|
3060
3492
|
if (routeRequest.clientTools?.length && output.toolCalls?.length) {
|
|
3061
3493
|
const loopResult = await executeClientToolLoop(
|
|
3062
3494
|
route,
|
|
@@ -3071,6 +3503,9 @@ function createClient(configOrInput, options = {}) {
|
|
|
3071
3503
|
transportSummary2 = loopResult.transportSummary;
|
|
3072
3504
|
}
|
|
3073
3505
|
}
|
|
3506
|
+
if (abort.signal.aborted) {
|
|
3507
|
+
throw mapAbortError(abort.reason);
|
|
3508
|
+
}
|
|
3074
3509
|
wideAttempts.push({
|
|
3075
3510
|
index: wideAttempts.length + 1,
|
|
3076
3511
|
route: routeSummary,
|
|
@@ -3141,6 +3576,49 @@ function createClient(configOrInput, options = {}) {
|
|
|
3141
3576
|
routeId: route.id
|
|
3142
3577
|
});
|
|
3143
3578
|
}
|
|
3579
|
+
if (error instanceof ClientToolExecutionError) {
|
|
3580
|
+
const toolError = error.aiError;
|
|
3581
|
+
attempts.push({
|
|
3582
|
+
routeId: route.id,
|
|
3583
|
+
handlerKey: route.handlerKey,
|
|
3584
|
+
errorCode: toolError.code,
|
|
3585
|
+
message: toolError.message
|
|
3586
|
+
});
|
|
3587
|
+
wideAttempts.push({
|
|
3588
|
+
index: wideAttempts.length + 1,
|
|
3589
|
+
route: routeSummary,
|
|
3590
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
3591
|
+
outcome: "error",
|
|
3592
|
+
retryCount,
|
|
3593
|
+
errorCode: toolError.code,
|
|
3594
|
+
message: toolError.message,
|
|
3595
|
+
...transportSummary2 ? { transport: transportSummary2 } : {}
|
|
3596
|
+
});
|
|
3597
|
+
await emitWideEvent({
|
|
3598
|
+
timestamp: isoTimestamp(startedAt),
|
|
3599
|
+
event: "ai_connect.operation",
|
|
3600
|
+
requestId,
|
|
3601
|
+
operationName,
|
|
3602
|
+
operation: normalizedRequest.operation,
|
|
3603
|
+
runtime: runtime.kind,
|
|
3604
|
+
outcome: "error",
|
|
3605
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
3606
|
+
candidates,
|
|
3607
|
+
attempts: wideAttempts,
|
|
3608
|
+
request: requestSummary,
|
|
3609
|
+
error: {
|
|
3610
|
+
code: toolError.code,
|
|
3611
|
+
message: toolError.message,
|
|
3612
|
+
routeId: route.id
|
|
3613
|
+
},
|
|
3614
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
3615
|
+
});
|
|
3616
|
+
throw new AiConnectError(toolError.code, toolError.message, {
|
|
3617
|
+
...toolError.details,
|
|
3618
|
+
attempts,
|
|
3619
|
+
routeId: route.id
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3144
3622
|
const normalizedError = toAiConnectError(error);
|
|
3145
3623
|
const actions = config.routing.fallback.on[normalizedError.code] ?? [];
|
|
3146
3624
|
attempts.push({
|
|
@@ -3385,30 +3863,16 @@ function createClient(configOrInput, options = {}) {
|
|
|
3385
3863
|
return report;
|
|
3386
3864
|
}
|
|
3387
3865
|
async function runPingWithAbort(route, handler, abort) {
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3866
|
+
return runWithAbortRace(
|
|
3867
|
+
handler({
|
|
3868
|
+
route,
|
|
3869
|
+
request: buildPingRequest(),
|
|
3870
|
+
runtime,
|
|
3871
|
+
attempt: 1,
|
|
3872
|
+
abort
|
|
3873
|
+
}),
|
|
3396
3874
|
abort
|
|
3397
|
-
|
|
3398
|
-
let onAbort;
|
|
3399
|
-
const abortPromise = new Promise((_, reject) => {
|
|
3400
|
-
onAbort = () => reject(mapAbortError(abort.reason));
|
|
3401
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
3402
|
-
});
|
|
3403
|
-
try {
|
|
3404
|
-
return await Promise.race([handlerPromise, abortPromise]);
|
|
3405
|
-
} finally {
|
|
3406
|
-
if (onAbort) {
|
|
3407
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
3408
|
-
}
|
|
3409
|
-
void Promise.resolve(handlerPromise).catch(() => {
|
|
3410
|
-
});
|
|
3411
|
-
}
|
|
3875
|
+
);
|
|
3412
3876
|
}
|
|
3413
3877
|
async function checkEndpointReachable(route, handler, abort) {
|
|
3414
3878
|
if (abort.signal.aborted) {
|
|
@@ -3951,12 +4415,6 @@ function createClient(configOrInput, options = {}) {
|
|
|
3951
4415
|
},
|
|
3952
4416
|
async *stream(request, opts) {
|
|
3953
4417
|
const preparedRequest = prepareRequest(request);
|
|
3954
|
-
if (preparedRequest.clientTools?.length) {
|
|
3955
|
-
throw new AiConnectError(
|
|
3956
|
-
"not_supported",
|
|
3957
|
-
"clientTools are currently supported only by generate(), not stream()."
|
|
3958
|
-
);
|
|
3959
|
-
}
|
|
3960
4418
|
const { abort, dispose: disposeAbort } = deriveAbort(
|
|
3961
4419
|
opts,
|
|
3962
4420
|
resolveOpTimeout(opts, "generateMs", options.timeouts),
|
|
@@ -3976,27 +4434,26 @@ function createClient(configOrInput, options = {}) {
|
|
|
3976
4434
|
release = await limiter.acquire(abort.signal);
|
|
3977
4435
|
const selectedModel = await resolveSelectedModel(preparedRequest);
|
|
3978
4436
|
const normalizedRequest = withSelectedModel(preparedRequest, selectedModel);
|
|
3979
|
-
const
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
3987
|
-
supportsToolSchema: true
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
3990
|
-
} : normalizedRequest
|
|
3991
|
-
);
|
|
4437
|
+
const {
|
|
4438
|
+
requiresImage,
|
|
4439
|
+
requiresFile,
|
|
4440
|
+
requiresClientTools,
|
|
4441
|
+
request: capabilityScopedRequest
|
|
4442
|
+
} = resolveRequiredCapabilities(normalizedRequest);
|
|
4443
|
+
const candidates = router.resolveCandidates(capabilityScopedRequest);
|
|
3992
4444
|
const startedAt = nowMs(runtime);
|
|
3993
4445
|
const requestId = resolveEventRequestId(normalizedRequest.logContext);
|
|
3994
4446
|
const candidateSummaries = candidates.map(summarizeRoute);
|
|
3995
4447
|
const requestSummary = summarizeRequest(normalizedRequest, candidateSummaries);
|
|
3996
4448
|
const attempts = [];
|
|
3997
4449
|
const wideAttempts = [];
|
|
4450
|
+
let hasYieldedOutward = false;
|
|
4451
|
+
let sawStreamHandler = false;
|
|
3998
4452
|
if (candidates.length === 0) {
|
|
3999
|
-
const error =
|
|
4453
|
+
const error = requiresImage || requiresFile || requiresClientTools ? new AiConnectError(
|
|
4454
|
+
"unsupported_capability",
|
|
4455
|
+
requiresImage || requiresFile ? `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".` : `No eligible route supports client-side tool execution for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
|
|
4456
|
+
) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
|
|
4000
4457
|
await emitWideEvent({
|
|
4001
4458
|
timestamp: isoTimestamp(startedAt),
|
|
4002
4459
|
event: "ai_connect.operation",
|
|
@@ -4023,84 +4480,202 @@ function createClient(configOrInput, options = {}) {
|
|
|
4023
4480
|
continue;
|
|
4024
4481
|
}
|
|
4025
4482
|
if (handler.stream) {
|
|
4483
|
+
sawStreamHandler = true;
|
|
4026
4484
|
const routeSummary = summarizeRoute(route);
|
|
4027
4485
|
const attemptStartedAt = nowMs(runtime);
|
|
4028
4486
|
let lastResult;
|
|
4029
4487
|
let accumulatedText = "";
|
|
4030
4488
|
const routeRequest = applyRouteDefaults(route, normalizedRequest);
|
|
4031
|
-
const
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
let iteratorReturned = false;
|
|
4040
|
-
const returnIterator = async () => {
|
|
4041
|
-
if (iteratorReturned) {
|
|
4042
|
-
return;
|
|
4043
|
-
}
|
|
4044
|
-
iteratorReturned = true;
|
|
4045
|
-
try {
|
|
4046
|
-
await iterator.return?.();
|
|
4047
|
-
} catch {
|
|
4048
|
-
}
|
|
4049
|
-
};
|
|
4489
|
+
const roundTools = routeRequest.clientTools ?? [];
|
|
4490
|
+
const registry = new Map(
|
|
4491
|
+
roundTools.map((tool) => [tool.function.name, tool])
|
|
4492
|
+
);
|
|
4493
|
+
let loopMessages = routeRequest.messages;
|
|
4494
|
+
let aggregatedUsage;
|
|
4495
|
+
const aggregatedWarnings = [];
|
|
4496
|
+
let toolRoundsExecuted = 0;
|
|
4050
4497
|
try {
|
|
4051
|
-
|
|
4498
|
+
roundsLoop: for (let round = 0; ; round += 1) {
|
|
4499
|
+
if (round >= MAX_CLIENT_TOOL_LOOP_DEPTH) {
|
|
4500
|
+
throw new ClientToolExecutionError(
|
|
4501
|
+
new AiConnectError(
|
|
4502
|
+
"validation_error",
|
|
4503
|
+
`clientTools exceeded the maximum tool-call loop depth of ${MAX_CLIENT_TOOL_LOOP_DEPTH}.`
|
|
4504
|
+
)
|
|
4505
|
+
);
|
|
4506
|
+
}
|
|
4052
4507
|
if (abort.signal.aborted) {
|
|
4053
|
-
await returnIterator();
|
|
4054
4508
|
throw mapAbortError(abort.reason);
|
|
4055
4509
|
}
|
|
4056
|
-
|
|
4510
|
+
const roundRequest = round === 0 ? routeRequest : { ...routeRequest, messages: loopMessages, attachments: [] };
|
|
4511
|
+
const iterable = handler.stream({
|
|
4512
|
+
route,
|
|
4513
|
+
request: roundRequest,
|
|
4514
|
+
runtime,
|
|
4515
|
+
attempt: 1,
|
|
4516
|
+
abort
|
|
4517
|
+
});
|
|
4518
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
4519
|
+
let iteratorReturned = false;
|
|
4520
|
+
const returnIterator = async () => {
|
|
4521
|
+
if (iteratorReturned) {
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
iteratorReturned = true;
|
|
4525
|
+
try {
|
|
4526
|
+
await iterator.return?.();
|
|
4527
|
+
} catch {
|
|
4528
|
+
}
|
|
4529
|
+
};
|
|
4530
|
+
let roundResult;
|
|
4531
|
+
try {
|
|
4532
|
+
while (true) {
|
|
4533
|
+
if (abort.signal.aborted) {
|
|
4534
|
+
await returnIterator();
|
|
4535
|
+
throw mapAbortError(abort.reason);
|
|
4536
|
+
}
|
|
4537
|
+
if (pauseSignal?.aborted) {
|
|
4538
|
+
await returnIterator();
|
|
4539
|
+
const pausedResult = lastResult ?? {
|
|
4540
|
+
route,
|
|
4541
|
+
text: accumulatedText,
|
|
4542
|
+
attachments: [],
|
|
4543
|
+
warnings: [],
|
|
4544
|
+
attempts
|
|
4545
|
+
};
|
|
4546
|
+
wideAttempts.push({
|
|
4547
|
+
index: wideAttempts.length + 1,
|
|
4548
|
+
route: routeSummary,
|
|
4549
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
4550
|
+
outcome: "success",
|
|
4551
|
+
retryCount: 0
|
|
4552
|
+
});
|
|
4553
|
+
await emitWideEvent({
|
|
4554
|
+
timestamp: isoTimestamp(startedAt),
|
|
4555
|
+
event: "ai_connect.operation",
|
|
4556
|
+
requestId,
|
|
4557
|
+
operationName: "stream",
|
|
4558
|
+
operation: normalizedRequest.operation,
|
|
4559
|
+
runtime: runtime.kind,
|
|
4560
|
+
outcome: "success",
|
|
4561
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
4562
|
+
candidates: candidateSummaries,
|
|
4563
|
+
attempts: wideAttempts,
|
|
4564
|
+
selectedRoute: routeSummary,
|
|
4565
|
+
request: requestSummary,
|
|
4566
|
+
result: summarizeResult(pausedResult),
|
|
4567
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
4568
|
+
});
|
|
4569
|
+
yield { type: "paused", result: pausedResult };
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
const next = await iterator.next();
|
|
4573
|
+
if (next.done) {
|
|
4574
|
+
break;
|
|
4575
|
+
}
|
|
4576
|
+
const event = next.value;
|
|
4577
|
+
if (event.type === "delta") {
|
|
4578
|
+
accumulatedText += event.text;
|
|
4579
|
+
hasYieldedOutward = true;
|
|
4580
|
+
yield event;
|
|
4581
|
+
continue;
|
|
4582
|
+
}
|
|
4583
|
+
if (event.type === "paused") {
|
|
4584
|
+
lastResult = event.result;
|
|
4585
|
+
wideAttempts.push({
|
|
4586
|
+
index: wideAttempts.length + 1,
|
|
4587
|
+
route: routeSummary,
|
|
4588
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
4589
|
+
outcome: "success",
|
|
4590
|
+
retryCount: 0
|
|
4591
|
+
});
|
|
4592
|
+
router.recordSuccess(route);
|
|
4593
|
+
await emitWideEvent({
|
|
4594
|
+
timestamp: isoTimestamp(startedAt),
|
|
4595
|
+
event: "ai_connect.operation",
|
|
4596
|
+
requestId,
|
|
4597
|
+
operationName: "stream",
|
|
4598
|
+
operation: normalizedRequest.operation,
|
|
4599
|
+
runtime: runtime.kind,
|
|
4600
|
+
outcome: "success",
|
|
4601
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
4602
|
+
candidates: candidateSummaries,
|
|
4603
|
+
attempts: wideAttempts,
|
|
4604
|
+
selectedRoute: routeSummary,
|
|
4605
|
+
request: requestSummary,
|
|
4606
|
+
result: summarizeResult(event.result),
|
|
4607
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
4608
|
+
});
|
|
4609
|
+
yield event;
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
if (event.type === "result") {
|
|
4613
|
+
roundResult = event.result;
|
|
4614
|
+
break;
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
} finally {
|
|
4057
4618
|
await returnIterator();
|
|
4058
|
-
const pausedResult = lastResult ?? {
|
|
4059
|
-
route,
|
|
4060
|
-
text: accumulatedText,
|
|
4061
|
-
attachments: [],
|
|
4062
|
-
warnings: [],
|
|
4063
|
-
attempts
|
|
4064
|
-
};
|
|
4065
|
-
wideAttempts.push({
|
|
4066
|
-
index: wideAttempts.length + 1,
|
|
4067
|
-
route: routeSummary,
|
|
4068
|
-
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
4069
|
-
outcome: "success",
|
|
4070
|
-
retryCount: 0
|
|
4071
|
-
});
|
|
4072
|
-
await emitWideEvent({
|
|
4073
|
-
timestamp: isoTimestamp(startedAt),
|
|
4074
|
-
event: "ai_connect.operation",
|
|
4075
|
-
requestId,
|
|
4076
|
-
operationName: "stream",
|
|
4077
|
-
operation: normalizedRequest.operation,
|
|
4078
|
-
runtime: runtime.kind,
|
|
4079
|
-
outcome: "success",
|
|
4080
|
-
durationMs: nowMs(runtime) - startedAt,
|
|
4081
|
-
candidates: candidateSummaries,
|
|
4082
|
-
attempts: wideAttempts,
|
|
4083
|
-
selectedRoute: routeSummary,
|
|
4084
|
-
request: requestSummary,
|
|
4085
|
-
result: summarizeResult(pausedResult),
|
|
4086
|
-
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
4087
|
-
});
|
|
4088
|
-
yield { type: "paused", result: pausedResult };
|
|
4089
|
-
return;
|
|
4090
4619
|
}
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4620
|
+
if (!roundResult) {
|
|
4621
|
+
break roundsLoop;
|
|
4622
|
+
}
|
|
4623
|
+
if (registry.size === 0 || !roundResult.toolCalls?.length) {
|
|
4624
|
+
if (toolRoundsExecuted === 0) {
|
|
4625
|
+
lastResult = roundResult;
|
|
4626
|
+
} else {
|
|
4627
|
+
const mergedUsage = mergeUsage(aggregatedUsage, roundResult.usage);
|
|
4628
|
+
lastResult = {
|
|
4629
|
+
...roundResult,
|
|
4630
|
+
warnings: [
|
|
4631
|
+
...aggregatedWarnings,
|
|
4632
|
+
...roundResult.warnings ?? []
|
|
4633
|
+
],
|
|
4634
|
+
...mergedUsage ? { usage: mergedUsage } : {}
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
yield { type: "result", result: lastResult };
|
|
4638
|
+
break roundsLoop;
|
|
4094
4639
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4640
|
+
aggregatedUsage = mergeUsage(aggregatedUsage, roundResult.usage);
|
|
4641
|
+
aggregatedWarnings.push(...roundResult.warnings ?? []);
|
|
4642
|
+
const roundGen = executeToolCallsRound({
|
|
4643
|
+
toolCalls: roundResult.toolCalls,
|
|
4644
|
+
assistantText: roundResult.text ?? "",
|
|
4645
|
+
registry,
|
|
4646
|
+
route,
|
|
4647
|
+
request: routeRequest,
|
|
4648
|
+
runtime,
|
|
4649
|
+
messages: loopMessages,
|
|
4650
|
+
abort,
|
|
4651
|
+
// D3/UR-007: emit a `tool-call` BEFORE execute, `tool-result` AFTER.
|
|
4652
|
+
onToolStart: (toolCall) => ({
|
|
4653
|
+
type: "tool-call",
|
|
4654
|
+
toolCall: {
|
|
4655
|
+
id: toolCall.id,
|
|
4656
|
+
name: toolCall.name,
|
|
4657
|
+
arguments: toolCall.arguments
|
|
4658
|
+
}
|
|
4659
|
+
}),
|
|
4660
|
+
onToolFinish: (toolCall, execution) => ({
|
|
4661
|
+
type: "tool-result",
|
|
4662
|
+
toolCallId: toolCall.id,
|
|
4663
|
+
name: toolCall.name,
|
|
4664
|
+
...execution.isError !== void 0 ? { isError: execution.isError } : {}
|
|
4665
|
+
})
|
|
4666
|
+
});
|
|
4667
|
+
let toolStep = await roundGen.next();
|
|
4668
|
+
while (!toolStep.done) {
|
|
4669
|
+
hasYieldedOutward = true;
|
|
4670
|
+
yield toolStep.value;
|
|
4671
|
+
toolStep = await roundGen.next();
|
|
4100
4672
|
}
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4673
|
+
loopMessages = [
|
|
4674
|
+
...loopMessages,
|
|
4675
|
+
toolStep.value.assistantMessage,
|
|
4676
|
+
...toolStep.value.toolMessages
|
|
4677
|
+
];
|
|
4678
|
+
toolRoundsExecuted += 1;
|
|
4104
4679
|
}
|
|
4105
4680
|
wideAttempts.push({
|
|
4106
4681
|
index: wideAttempts.length + 1,
|
|
@@ -4128,7 +4703,44 @@ function createClient(configOrInput, options = {}) {
|
|
|
4128
4703
|
});
|
|
4129
4704
|
return;
|
|
4130
4705
|
} catch (error) {
|
|
4131
|
-
|
|
4706
|
+
if (error instanceof ClientToolExecutionError && !abort.signal.aborted) {
|
|
4707
|
+
const toolError = error.aiError;
|
|
4708
|
+
attempts.push({
|
|
4709
|
+
routeId: route.id,
|
|
4710
|
+
handlerKey: route.handlerKey,
|
|
4711
|
+
errorCode: toolError.code,
|
|
4712
|
+
message: toolError.message
|
|
4713
|
+
});
|
|
4714
|
+
wideAttempts.push({
|
|
4715
|
+
index: wideAttempts.length + 1,
|
|
4716
|
+
route: routeSummary,
|
|
4717
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
4718
|
+
outcome: "error",
|
|
4719
|
+
retryCount: 0,
|
|
4720
|
+
errorCode: toolError.code,
|
|
4721
|
+
message: toolError.message
|
|
4722
|
+
});
|
|
4723
|
+
await emitWideEvent({
|
|
4724
|
+
timestamp: isoTimestamp(startedAt),
|
|
4725
|
+
event: "ai_connect.operation",
|
|
4726
|
+
requestId,
|
|
4727
|
+
operationName: "stream",
|
|
4728
|
+
operation: normalizedRequest.operation,
|
|
4729
|
+
runtime: runtime.kind,
|
|
4730
|
+
outcome: "error",
|
|
4731
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
4732
|
+
candidates: candidateSummaries,
|
|
4733
|
+
attempts: wideAttempts,
|
|
4734
|
+
request: requestSummary,
|
|
4735
|
+
error: {
|
|
4736
|
+
code: toolError.code,
|
|
4737
|
+
message: toolError.message,
|
|
4738
|
+
routeId: route.id
|
|
4739
|
+
},
|
|
4740
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
4741
|
+
});
|
|
4742
|
+
throw toolError;
|
|
4743
|
+
}
|
|
4132
4744
|
const normalizedError = abort.signal.aborted ? mapAbortError(abort.reason) : toAiConnectError(error);
|
|
4133
4745
|
attempts.push({
|
|
4134
4746
|
routeId: route.id,
|
|
@@ -4167,6 +4779,28 @@ function createClient(configOrInput, options = {}) {
|
|
|
4167
4779
|
});
|
|
4168
4780
|
throw normalizedError;
|
|
4169
4781
|
}
|
|
4782
|
+
if (hasYieldedOutward) {
|
|
4783
|
+
await emitWideEvent({
|
|
4784
|
+
timestamp: isoTimestamp(startedAt),
|
|
4785
|
+
event: "ai_connect.operation",
|
|
4786
|
+
requestId,
|
|
4787
|
+
operationName: "stream",
|
|
4788
|
+
operation: normalizedRequest.operation,
|
|
4789
|
+
runtime: runtime.kind,
|
|
4790
|
+
outcome: "error",
|
|
4791
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
4792
|
+
candidates: candidateSummaries,
|
|
4793
|
+
attempts: wideAttempts,
|
|
4794
|
+
request: requestSummary,
|
|
4795
|
+
error: {
|
|
4796
|
+
code: normalizedError.code,
|
|
4797
|
+
message: normalizedError.message,
|
|
4798
|
+
routeId: route.id
|
|
4799
|
+
},
|
|
4800
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
4801
|
+
});
|
|
4802
|
+
throw normalizedError;
|
|
4803
|
+
}
|
|
4170
4804
|
router.recordFailure(route, normalizedError.code);
|
|
4171
4805
|
continue;
|
|
4172
4806
|
}
|
|
@@ -4182,6 +4816,12 @@ function createClient(configOrInput, options = {}) {
|
|
|
4182
4816
|
attempts,
|
|
4183
4817
|
wideAttempts
|
|
4184
4818
|
});
|
|
4819
|
+
if (requiresClientTools && !sawStreamHandler) {
|
|
4820
|
+
result.warnings = [
|
|
4821
|
+
...result.warnings,
|
|
4822
|
+
"Streaming tool-loop is not supported by the available route(s); returned a non-streamed result via the generate tool-loop."
|
|
4823
|
+
];
|
|
4824
|
+
}
|
|
4185
4825
|
yield {
|
|
4186
4826
|
type: "result",
|
|
4187
4827
|
result
|
|
@@ -4472,12 +5112,32 @@ function trimTrailingSlash(value) {
|
|
|
4472
5112
|
function appendPath(baseUrl, suffix) {
|
|
4473
5113
|
return `${trimTrailingSlash(baseUrl)}/${suffix.replace(/^\/+/, "")}`;
|
|
4474
5114
|
}
|
|
5115
|
+
function isAbortError(error) {
|
|
5116
|
+
if (error instanceof AiConnectError) {
|
|
5117
|
+
return error.code === "aborted";
|
|
5118
|
+
}
|
|
5119
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
5120
|
+
}
|
|
5121
|
+
function attachPartsToLastUserTurn(conversation, createEmptyUserTurn) {
|
|
5122
|
+
let targetIndex = -1;
|
|
5123
|
+
for (let index = conversation.length - 1; index >= 0; index -= 1) {
|
|
5124
|
+
if (conversation[index]?.role === "user") {
|
|
5125
|
+
targetIndex = index;
|
|
5126
|
+
break;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
if (targetIndex === -1) {
|
|
5130
|
+
conversation.push(createEmptyUserTurn());
|
|
5131
|
+
targetIndex = conversation.length - 1;
|
|
5132
|
+
}
|
|
5133
|
+
return conversation[targetIndex];
|
|
5134
|
+
}
|
|
4475
5135
|
function resolveOpenAiEndpoint(route, operation) {
|
|
4476
5136
|
const configured = route.transport.baseUrl?.trim();
|
|
4477
5137
|
if (!configured) {
|
|
4478
5138
|
return operation === "image" ? "https://api.openai.com/v1/images/generations" : "https://api.openai.com/v1/chat/completions";
|
|
4479
5139
|
}
|
|
4480
|
-
if (configured.endsWith("/chat/completions") || configured.endsWith("/images/generations")) {
|
|
5140
|
+
if (operation === "text" && configured.endsWith("/chat/completions") || operation === "image" && configured.endsWith("/images/generations")) {
|
|
4481
5141
|
return configured;
|
|
4482
5142
|
}
|
|
4483
5143
|
return appendPath(
|
|
@@ -5308,7 +5968,7 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5308
5968
|
}
|
|
5309
5969
|
try {
|
|
5310
5970
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5311
|
-
const root = base ? base.replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5971
|
+
const root = base ? trimTrailingSlash(base).replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5312
5972
|
const endpoint = appendPath(root, "/files");
|
|
5313
5973
|
const form = new FormData();
|
|
5314
5974
|
form.append(
|
|
@@ -5332,7 +5992,10 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5332
5992
|
}
|
|
5333
5993
|
const id = recordOf(data).id;
|
|
5334
5994
|
return typeof id === "string" ? id : void 0;
|
|
5335
|
-
} catch {
|
|
5995
|
+
} catch (error) {
|
|
5996
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5997
|
+
throw error;
|
|
5998
|
+
}
|
|
5336
5999
|
return void 0;
|
|
5337
6000
|
}
|
|
5338
6001
|
}
|
|
@@ -5365,7 +6028,10 @@ async function uploadOpenAiFile(payload, uploader) {
|
|
|
5365
6028
|
}
|
|
5366
6029
|
const id = recordOf(data).id;
|
|
5367
6030
|
return typeof id === "string" ? id : void 0;
|
|
5368
|
-
} catch {
|
|
6031
|
+
} catch (error) {
|
|
6032
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
6033
|
+
throw error;
|
|
6034
|
+
}
|
|
5369
6035
|
return void 0;
|
|
5370
6036
|
}
|
|
5371
6037
|
}
|
|
@@ -5375,10 +6041,11 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5375
6041
|
}
|
|
5376
6042
|
try {
|
|
5377
6043
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5378
|
-
const
|
|
5379
|
-
|
|
6044
|
+
const root = base ? trimTrailingSlash(base.replace(/\/v1beta\/models.*$/, "/v1beta")) : void 0;
|
|
6045
|
+
const endpoint = root ? appendPath(
|
|
6046
|
+
root.endsWith("/v1beta") ? `${root.slice(0, -"/v1beta".length)}/upload/v1beta` : `${root}/upload`,
|
|
5380
6047
|
"/files"
|
|
5381
|
-
)
|
|
6048
|
+
) : "https://generativelanguage.googleapis.com/upload/v1beta/files";
|
|
5382
6049
|
const response = await uploader.fetchImpl(endpoint, {
|
|
5383
6050
|
method: "POST",
|
|
5384
6051
|
headers: {
|
|
@@ -5397,7 +6064,10 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5397
6064
|
const file = recordOf(recordOf(data).file);
|
|
5398
6065
|
const uri = file.uri ?? recordOf(data).uri;
|
|
5399
6066
|
return typeof uri === "string" ? uri : void 0;
|
|
5400
|
-
} catch {
|
|
6067
|
+
} catch (error) {
|
|
6068
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
6069
|
+
throw error;
|
|
6070
|
+
}
|
|
5401
6071
|
return void 0;
|
|
5402
6072
|
}
|
|
5403
6073
|
}
|
|
@@ -5595,21 +6265,10 @@ function attachOpenAiParts(conversation, parts) {
|
|
|
5595
6265
|
if (parts.length === 0) {
|
|
5596
6266
|
return conversation;
|
|
5597
6267
|
}
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
break;
|
|
5603
|
-
}
|
|
5604
|
-
}
|
|
5605
|
-
if (targetIndex === -1) {
|
|
5606
|
-
conversation.push({
|
|
5607
|
-
role: "user",
|
|
5608
|
-
content: ""
|
|
5609
|
-
});
|
|
5610
|
-
targetIndex = conversation.length - 1;
|
|
5611
|
-
}
|
|
5612
|
-
const target = conversation[targetIndex];
|
|
6268
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6269
|
+
role: "user",
|
|
6270
|
+
content: ""
|
|
6271
|
+
}));
|
|
5613
6272
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5614
6273
|
target.content = [...baseParts, ...parts];
|
|
5615
6274
|
return conversation;
|
|
@@ -5695,21 +6354,10 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5695
6354
|
}
|
|
5696
6355
|
const attachmentParts = await anthropicAttachmentParts(attachments, uploader);
|
|
5697
6356
|
if (attachmentParts.length > 0) {
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
break;
|
|
5703
|
-
}
|
|
5704
|
-
}
|
|
5705
|
-
if (targetIndex === -1) {
|
|
5706
|
-
conversation.push({
|
|
5707
|
-
role: "user",
|
|
5708
|
-
content: ""
|
|
5709
|
-
});
|
|
5710
|
-
targetIndex = conversation.length - 1;
|
|
5711
|
-
}
|
|
5712
|
-
const target = conversation[targetIndex];
|
|
6357
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6358
|
+
role: "user",
|
|
6359
|
+
content: ""
|
|
6360
|
+
}));
|
|
5713
6361
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5714
6362
|
target.content = [...baseParts, ...attachmentParts];
|
|
5715
6363
|
}
|
|
@@ -5720,12 +6368,27 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5720
6368
|
}
|
|
5721
6369
|
async function geminiRequest(messages, attachments, uploader) {
|
|
5722
6370
|
const system = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n");
|
|
6371
|
+
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
6372
|
+
for (const message of messages) {
|
|
6373
|
+
if (hasAssistantToolCalls(message)) {
|
|
6374
|
+
for (const call of message.toolCalls) {
|
|
6375
|
+
toolNameByCallId.set(call.id, call.name);
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
5723
6379
|
const contents = messages.filter((message) => message.role !== "system").reduce(
|
|
5724
6380
|
(conversation, message) => {
|
|
5725
6381
|
if (isToolMessage(message)) {
|
|
6382
|
+
const resolvedName = message.toolName ?? toolNameByCallId.get(message.toolCallId);
|
|
6383
|
+
if (!resolvedName) {
|
|
6384
|
+
throw new AiConnectError(
|
|
6385
|
+
"validation_error",
|
|
6386
|
+
`Gemini functionResponse requires a tool name: the tool message for toolCallId "${message.toolCallId}" has no toolName and no matching assistant toolCall was found.`
|
|
6387
|
+
);
|
|
6388
|
+
}
|
|
5726
6389
|
const response = {
|
|
5727
6390
|
functionResponse: {
|
|
5728
|
-
name:
|
|
6391
|
+
name: resolvedName,
|
|
5729
6392
|
response: isRecord(message.data) ? message.data : {
|
|
5730
6393
|
content: message.content,
|
|
5731
6394
|
...message.isError !== void 0 ? { isError: message.isError } : {}
|
|
@@ -5772,21 +6435,11 @@ async function geminiRequest(messages, attachments, uploader) {
|
|
|
5772
6435
|
);
|
|
5773
6436
|
const attachmentParts = await geminiAttachmentParts(attachments, uploader);
|
|
5774
6437
|
if (attachmentParts.length > 0) {
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
}
|
|
5781
|
-
}
|
|
5782
|
-
if (targetIndex === -1) {
|
|
5783
|
-
contents.push({
|
|
5784
|
-
role: "user",
|
|
5785
|
-
parts: []
|
|
5786
|
-
});
|
|
5787
|
-
targetIndex = contents.length - 1;
|
|
5788
|
-
}
|
|
5789
|
-
contents[targetIndex].parts.push(...attachmentParts);
|
|
6438
|
+
const target = attachPartsToLastUserTurn(contents, () => ({
|
|
6439
|
+
role: "user",
|
|
6440
|
+
parts: []
|
|
6441
|
+
}));
|
|
6442
|
+
target.parts.push(...attachmentParts);
|
|
5790
6443
|
}
|
|
5791
6444
|
return {
|
|
5792
6445
|
...system ? {
|
|
@@ -5836,6 +6489,22 @@ function extractThoughts(value) {
|
|
|
5836
6489
|
const joined = thoughts.join("\n").trim();
|
|
5837
6490
|
return joined.length > 0 ? joined : void 0;
|
|
5838
6491
|
}
|
|
6492
|
+
function extractThinking(value) {
|
|
6493
|
+
if (!Array.isArray(value)) {
|
|
6494
|
+
return void 0;
|
|
6495
|
+
}
|
|
6496
|
+
const thinking = value.flatMap((part) => {
|
|
6497
|
+
if (part && typeof part === "object" && part.type === "thinking") {
|
|
6498
|
+
const candidate = part.thinking;
|
|
6499
|
+
if (typeof candidate === "string") {
|
|
6500
|
+
return [candidate];
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
return [];
|
|
6504
|
+
});
|
|
6505
|
+
const joined = thinking.join("\n").trim();
|
|
6506
|
+
return joined.length > 0 ? joined : void 0;
|
|
6507
|
+
}
|
|
5839
6508
|
function imageAttachmentsFromPayload(payload, ...sources) {
|
|
5840
6509
|
return extractImagePayloads(payload, ...sources).map(
|
|
5841
6510
|
(item) => preparePortableFile(item)
|
|
@@ -5923,6 +6592,10 @@ function buildOpenAiChatBody(context, messages, parameters, responseFormat, opts
|
|
|
5923
6592
|
model: context.route.model,
|
|
5924
6593
|
messages,
|
|
5925
6594
|
stream: opts.stream,
|
|
6595
|
+
// Ask OpenAI-compatible streaming routes for the trailing usage chunk; without
|
|
6596
|
+
// `stream_options.include_usage` the SSE stream carries no `usage` block and
|
|
6597
|
+
// `runOpenAiStream` would always report `usage: undefined` (H5).
|
|
6598
|
+
...opts.stream ? { stream_options: { include_usage: true } } : {},
|
|
5926
6599
|
...parameters?.maxTokens !== void 0 ? { max_tokens: parameters.maxTokens } : {},
|
|
5927
6600
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
5928
6601
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6077,6 +6750,11 @@ async function* runOpenAiStream(fetchImpl, context) {
|
|
|
6077
6750
|
}
|
|
6078
6751
|
const response = outcome.response;
|
|
6079
6752
|
const streamWarnings = [...uploaderWarnings];
|
|
6753
|
+
if (parameters?.candidateCount && parameters.candidateCount > 1) {
|
|
6754
|
+
streamWarnings.push(
|
|
6755
|
+
"candidateCount > 1 is forwarded to OpenAI-compatible text routes, but the normalized result returns only the first choice."
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6080
6758
|
if (droppedResponseFormat) {
|
|
6081
6759
|
streamWarnings.push(
|
|
6082
6760
|
"response_format was rejected by the OpenAI-compatible route (HTTP 400); the request was retried once without response_format and structured-output formatting was NOT enforced."
|
|
@@ -6222,8 +6900,7 @@ function buildOpenAiStreamResult(context, text, toolCalls, usage, warnings) {
|
|
|
6222
6900
|
} : {}
|
|
6223
6901
|
};
|
|
6224
6902
|
}
|
|
6225
|
-
async function
|
|
6226
|
-
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6903
|
+
async function runProviderImage(fetchImpl, context, opts) {
|
|
6227
6904
|
const referenceImages = imageReferenceFiles(context);
|
|
6228
6905
|
const bundle = buildImagePromptBundle({
|
|
6229
6906
|
request: context.request,
|
|
@@ -6232,10 +6909,10 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6232
6909
|
});
|
|
6233
6910
|
const baseFields = buildImageRequestFields(context, bundle);
|
|
6234
6911
|
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6235
|
-
const endpoint = useEditEndpoint ?
|
|
6912
|
+
const endpoint = useEditEndpoint ? opts.resolveEditEndpoint(context.route) : opts.resolveEndpoint(context.route);
|
|
6236
6913
|
transportSummary(
|
|
6237
6914
|
context,
|
|
6238
|
-
useEditEndpoint ?
|
|
6915
|
+
useEditEndpoint ? `${opts.providerLabel}-image-edit` : `${opts.providerLabel}-image`,
|
|
6239
6916
|
endpoint,
|
|
6240
6917
|
"POST",
|
|
6241
6918
|
false,
|
|
@@ -6248,23 +6925,21 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6248
6925
|
);
|
|
6249
6926
|
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6250
6927
|
method: "POST",
|
|
6251
|
-
headers: {
|
|
6252
|
-
authorization: `Bearer ${apiKey}`
|
|
6253
|
-
},
|
|
6928
|
+
headers: { ...opts.authHeaders },
|
|
6254
6929
|
body: await buildImageEditFormData(context, baseFields),
|
|
6255
6930
|
signal: context.abort.signal
|
|
6256
6931
|
} : {
|
|
6257
6932
|
method: "POST",
|
|
6258
6933
|
headers: {
|
|
6259
6934
|
"content-type": "application/json",
|
|
6260
|
-
|
|
6935
|
+
...opts.authHeaders
|
|
6261
6936
|
},
|
|
6262
6937
|
body: JSON.stringify(baseFields),
|
|
6263
6938
|
signal: context.abort.signal
|
|
6264
6939
|
});
|
|
6265
6940
|
const payload = await readPayload(response);
|
|
6266
6941
|
if (!response.ok) {
|
|
6267
|
-
throw classifyApiError(
|
|
6942
|
+
throw classifyApiError(opts.classifyProvider, response, payload);
|
|
6268
6943
|
}
|
|
6269
6944
|
return {
|
|
6270
6945
|
data: {
|
|
@@ -6274,6 +6949,16 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6274
6949
|
attachments: imageAttachmentsFromPayload(payload)
|
|
6275
6950
|
};
|
|
6276
6951
|
}
|
|
6952
|
+
async function runOpenAiImage(fetchImpl, context) {
|
|
6953
|
+
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6954
|
+
return runProviderImage(fetchImpl, context, {
|
|
6955
|
+
resolveEndpoint: (route) => resolveOpenAiEndpoint(route, "image"),
|
|
6956
|
+
resolveEditEndpoint: (route) => resolveOpenAiImageEditEndpoint(route),
|
|
6957
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6958
|
+
providerLabel: "openai",
|
|
6959
|
+
classifyProvider: "openai"
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6277
6962
|
async function runAnthropic(fetchImpl, context) {
|
|
6278
6963
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6279
6964
|
const uploaderWarnings = [];
|
|
@@ -6285,6 +6970,8 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6285
6970
|
const parameters = context.request.parameters;
|
|
6286
6971
|
const requestBody = {
|
|
6287
6972
|
model: context.route.model,
|
|
6973
|
+
// Anthropic REQUIRES `max_tokens` (unlike OpenAI/Gemini, where it is optional), so a
|
|
6974
|
+
// provider-specific default of 4096 is applied when the caller omits `maxTokens`.
|
|
6288
6975
|
max_tokens: parameters?.maxTokens ?? 4096,
|
|
6289
6976
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
6290
6977
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6314,8 +7001,12 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6314
7001
|
throw classifyApiError("anthropic", response, payload);
|
|
6315
7002
|
}
|
|
6316
7003
|
const data = payload;
|
|
7004
|
+
const anthropicReasoning = extractThinking(data.content);
|
|
6317
7005
|
return {
|
|
6318
7006
|
text: extractText(data.content),
|
|
7007
|
+
// Anthropic extended-thinking blocks, separated from the answer (consumers route this
|
|
7008
|
+
// to a thinking UI), mirroring the Gemini `reasoning` path.
|
|
7009
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
|
|
6319
7010
|
data,
|
|
6320
7011
|
...anthropicToolCallsFromPayload(data).length > 0 ? { toolCalls: anthropicToolCallsFromPayload(data) } : {},
|
|
6321
7012
|
...uploaderWarnings.length > 0 ? { warnings: uploaderWarnings } : {},
|
|
@@ -6410,55 +7101,13 @@ async function runGemini(fetchImpl, context) {
|
|
|
6410
7101
|
}
|
|
6411
7102
|
async function runGeminiImage(fetchImpl, context) {
|
|
6412
7103
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
7104
|
+
return runProviderImage(fetchImpl, context, {
|
|
7105
|
+
resolveEndpoint: (route) => resolveGeminiImageEndpoint(route),
|
|
7106
|
+
resolveEditEndpoint: (route) => resolveGeminiImageEditEndpoint(route),
|
|
7107
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
7108
|
+
providerLabel: "gemini",
|
|
7109
|
+
classifyProvider: context.route.provider
|
|
6418
7110
|
});
|
|
6419
|
-
const baseFields = buildImageRequestFields(context, bundle);
|
|
6420
|
-
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6421
|
-
const endpoint = useEditEndpoint ? resolveGeminiImageEditEndpoint(context.route) : resolveGeminiImageEndpoint(context.route);
|
|
6422
|
-
transportSummary(
|
|
6423
|
-
context,
|
|
6424
|
-
useEditEndpoint ? "gemini-image-edit" : "gemini-image",
|
|
6425
|
-
endpoint,
|
|
6426
|
-
"POST",
|
|
6427
|
-
false,
|
|
6428
|
-
{
|
|
6429
|
-
...baseFields,
|
|
6430
|
-
source_image: Boolean(context.request.image?.sourceImage),
|
|
6431
|
-
mask: Boolean(context.request.image?.mask),
|
|
6432
|
-
reference_image_count: referenceImages.length
|
|
6433
|
-
}
|
|
6434
|
-
);
|
|
6435
|
-
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6436
|
-
method: "POST",
|
|
6437
|
-
headers: {
|
|
6438
|
-
authorization: `Bearer ${apiKey}`
|
|
6439
|
-
},
|
|
6440
|
-
body: await buildImageEditFormData(context, baseFields),
|
|
6441
|
-
signal: context.abort.signal
|
|
6442
|
-
} : {
|
|
6443
|
-
method: "POST",
|
|
6444
|
-
headers: {
|
|
6445
|
-
"content-type": "application/json",
|
|
6446
|
-
authorization: `Bearer ${apiKey}`
|
|
6447
|
-
},
|
|
6448
|
-
body: JSON.stringify(baseFields),
|
|
6449
|
-
signal: context.abort.signal
|
|
6450
|
-
});
|
|
6451
|
-
const payload = await readPayload(response);
|
|
6452
|
-
if (!response.ok) {
|
|
6453
|
-
throw classifyApiError(context.route.provider, response, payload);
|
|
6454
|
-
}
|
|
6455
|
-
return {
|
|
6456
|
-
data: {
|
|
6457
|
-
request: bundle,
|
|
6458
|
-
response: payload
|
|
6459
|
-
},
|
|
6460
|
-
attachments: imageAttachmentsFromPayload(payload)
|
|
6461
|
-
};
|
|
6462
7111
|
}
|
|
6463
7112
|
function verifyApiRoute(route, runtime) {
|
|
6464
7113
|
resolveApiKey(route, runtime);
|
|
@@ -6537,9 +7186,12 @@ var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
|
|
|
6537
7186
|
transportId: "opencode-server"
|
|
6538
7187
|
}
|
|
6539
7188
|
};
|
|
6540
|
-
var AI_CONNECT_DEFAULT_SERVER_COMMANDS =
|
|
6541
|
-
|
|
6542
|
-
}
|
|
7189
|
+
var AI_CONNECT_DEFAULT_SERVER_COMMANDS = Object.fromEntries(
|
|
7190
|
+
Object.values(AI_CONNECT_DEFAULT_SERVER_PRESETS).map((preset) => [
|
|
7191
|
+
`${preset.id}:${preset.transportId}`,
|
|
7192
|
+
preset.command
|
|
7193
|
+
])
|
|
7194
|
+
);
|
|
6543
7195
|
|
|
6544
7196
|
// src/catalog.ts
|
|
6545
7197
|
var TEXT_PROVIDER_CATALOG = [
|