@vedmalex/ai-connect 0.8.0 → 0.9.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 +717 -284
- package/dist/browser/index.js.map +3 -3
- package/dist/bun/index.js +1089 -440
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/local.js +1041 -440
- package/dist/bun/local.js.map +4 -4
- package/dist/node/index.js +1089 -440
- package/dist/node/index.js.map +4 -4
- package/dist/node/local.js +1041 -440
- 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 +18 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bun/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) {
|
|
@@ -1202,19 +1337,54 @@ function isRemoteUrl(value) {
|
|
|
1202
1337
|
return false;
|
|
1203
1338
|
}
|
|
1204
1339
|
}
|
|
1340
|
+
function mimeTypeAndExtensionFromMediaType(mediaTypeAndParams, fallbackMimeType, fallbackExtension) {
|
|
1341
|
+
const mimeType = mediaTypeAndParams.split(";")[0]?.trim() || fallbackMimeType;
|
|
1342
|
+
const extension2 = mimeType.split("/")[1] ?? fallbackExtension;
|
|
1343
|
+
return { mimeType, extension: extension2 };
|
|
1344
|
+
}
|
|
1205
1345
|
function parseDataUrl(value) {
|
|
1206
|
-
const
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1346
|
+
const base64Match = /^data:([^,]*?);base64,(.*)$/i.exec(value);
|
|
1347
|
+
if (base64Match) {
|
|
1348
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1349
|
+
base64Match[1] ?? "",
|
|
1350
|
+
"application/octet-stream",
|
|
1351
|
+
"bin"
|
|
1352
|
+
);
|
|
1353
|
+
return {
|
|
1354
|
+
mimeType,
|
|
1355
|
+
extension: extension2,
|
|
1356
|
+
data: base64Match[2] ?? ""
|
|
1357
|
+
};
|
|
1209
1358
|
}
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1359
|
+
const plainMatch = /^data:([^,]*?),(.*)$/i.exec(value);
|
|
1360
|
+
if (plainMatch) {
|
|
1361
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1362
|
+
plainMatch[1] ?? "",
|
|
1363
|
+
"text/plain",
|
|
1364
|
+
"txt"
|
|
1365
|
+
);
|
|
1366
|
+
let decoded;
|
|
1367
|
+
try {
|
|
1368
|
+
decoded = decodeURIComponent(plainMatch[2] ?? "");
|
|
1369
|
+
} catch {
|
|
1370
|
+
throw new AiConnectError(
|
|
1371
|
+
"validation_error",
|
|
1372
|
+
"unsupported data URL encoding"
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
mimeType,
|
|
1377
|
+
extension: extension2,
|
|
1378
|
+
data: encodeBase64(new TextEncoder().encode(decoded))
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
if (/^data:/i.test(value)) {
|
|
1382
|
+
throw new AiConnectError(
|
|
1383
|
+
"validation_error",
|
|
1384
|
+
"unsupported data URL encoding"
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
return null;
|
|
1218
1388
|
}
|
|
1219
1389
|
function mimeTypeFromName(name) {
|
|
1220
1390
|
const ext = extension(name);
|
|
@@ -1428,9 +1598,61 @@ async function materializePortableFile(file) {
|
|
|
1428
1598
|
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1429
1599
|
}
|
|
1430
1600
|
}
|
|
1601
|
+
if (category === "other") {
|
|
1602
|
+
const base64 = await portableFileToBase64(file);
|
|
1603
|
+
if (base64 !== void 0) {
|
|
1604
|
+
payload.base64 = base64;
|
|
1605
|
+
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1606
|
+
} else if (!uri && !providerFileId) {
|
|
1607
|
+
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.`;
|
|
1608
|
+
payload.droppedReason = droppedReason;
|
|
1609
|
+
payload.warnings = [...payload.warnings ?? [], droppedReason];
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1431
1612
|
return payload;
|
|
1432
1613
|
}
|
|
1433
|
-
function
|
|
1614
|
+
function collapsePathSegments(value) {
|
|
1615
|
+
const normalized = value.replaceAll("\\", "/");
|
|
1616
|
+
const isAbsolute = normalized.startsWith("/");
|
|
1617
|
+
const segments = normalized.split("/");
|
|
1618
|
+
const resolved = [];
|
|
1619
|
+
for (const segment of segments) {
|
|
1620
|
+
if (segment === "" || segment === ".") {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (segment === "..") {
|
|
1624
|
+
if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
|
|
1625
|
+
resolved.pop();
|
|
1626
|
+
} else if (!isAbsolute) {
|
|
1627
|
+
resolved.push("..");
|
|
1628
|
+
}
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
resolved.push(segment);
|
|
1632
|
+
}
|
|
1633
|
+
return (isAbsolute ? "/" : "") + resolved.join("/");
|
|
1634
|
+
}
|
|
1635
|
+
function applyPathGuard(rawPath, guard) {
|
|
1636
|
+
if (!guard) {
|
|
1637
|
+
return rawPath;
|
|
1638
|
+
}
|
|
1639
|
+
if (guard === "basename") {
|
|
1640
|
+
return basename(rawPath);
|
|
1641
|
+
}
|
|
1642
|
+
const resolved = collapsePathSegments(rawPath);
|
|
1643
|
+
const withinAllowedRoot = guard.allowedRoots.some((root) => {
|
|
1644
|
+
const resolvedRoot = collapsePathSegments(root);
|
|
1645
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}/`);
|
|
1646
|
+
});
|
|
1647
|
+
if (!withinAllowedRoot) {
|
|
1648
|
+
throw new AiConnectError(
|
|
1649
|
+
"validation_error",
|
|
1650
|
+
`Path "${rawPath}" is outside the configured allowedRoots for portable file attachments.`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
return rawPath;
|
|
1654
|
+
}
|
|
1655
|
+
function preparePortableFile(input, options) {
|
|
1434
1656
|
if (isPortableFile(input)) {
|
|
1435
1657
|
return input;
|
|
1436
1658
|
}
|
|
@@ -1456,11 +1678,12 @@ function preparePortableFile(input) {
|
|
|
1456
1678
|
}
|
|
1457
1679
|
};
|
|
1458
1680
|
}
|
|
1681
|
+
const guardedPath = applyPathGuard(input, options?.pathGuard);
|
|
1459
1682
|
return {
|
|
1460
|
-
id: `path:${
|
|
1683
|
+
id: `path:${guardedPath}`,
|
|
1461
1684
|
kind: "path",
|
|
1462
|
-
name: basename(
|
|
1463
|
-
source:
|
|
1685
|
+
name: basename(guardedPath),
|
|
1686
|
+
source: guardedPath
|
|
1464
1687
|
};
|
|
1465
1688
|
}
|
|
1466
1689
|
if (isRemoteReference(input)) {
|
|
@@ -1501,17 +1724,76 @@ function preparePortableFile(input) {
|
|
|
1501
1724
|
}
|
|
1502
1725
|
|
|
1503
1726
|
// src/logging.ts
|
|
1727
|
+
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
1728
|
+
"apikey",
|
|
1729
|
+
"authorization",
|
|
1730
|
+
"password",
|
|
1731
|
+
"token",
|
|
1732
|
+
"x-api-key",
|
|
1733
|
+
"x-goog-api-key"
|
|
1734
|
+
]);
|
|
1735
|
+
var REDACTED = "[REDACTED]";
|
|
1736
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1737
|
+
var SECRET_PREFIX_RE = /^(sk-|sk-ant-|xox[baprs]-|ghp_|gho_|AIza)/;
|
|
1738
|
+
function looksLikeSecretValue(value) {
|
|
1739
|
+
if (UUID_RE.test(value)) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
if (/^Bearer\s+/i.test(value)) {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
if (SECRET_PREFIX_RE.test(value)) {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
return /^[A-Za-z0-9+/=_]{40,}$/.test(value);
|
|
1749
|
+
}
|
|
1750
|
+
function redactNode(node) {
|
|
1751
|
+
if (node === null || node === void 0) {
|
|
1752
|
+
return node;
|
|
1753
|
+
}
|
|
1754
|
+
if (typeof node === "string") {
|
|
1755
|
+
return looksLikeSecretValue(node) ? REDACTED : node;
|
|
1756
|
+
}
|
|
1757
|
+
if (typeof node !== "object") {
|
|
1758
|
+
return node;
|
|
1759
|
+
}
|
|
1760
|
+
if (Array.isArray(node)) {
|
|
1761
|
+
return node.map((item) => redactNode(item));
|
|
1762
|
+
}
|
|
1763
|
+
const result = {};
|
|
1764
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1765
|
+
result[key] = REDACT_KEYS.has(key.toLowerCase()) ? REDACTED : redactNode(value);
|
|
1766
|
+
}
|
|
1767
|
+
return result;
|
|
1768
|
+
}
|
|
1769
|
+
function redactEvent(event) {
|
|
1770
|
+
return redactNode(event);
|
|
1771
|
+
}
|
|
1772
|
+
function redactBaseUrl(url) {
|
|
1773
|
+
try {
|
|
1774
|
+
const parsed = new URL(url);
|
|
1775
|
+
parsed.username = "";
|
|
1776
|
+
parsed.password = "";
|
|
1777
|
+
parsed.search = "";
|
|
1778
|
+
parsed.hash = "";
|
|
1779
|
+
return parsed.toString();
|
|
1780
|
+
} catch {
|
|
1781
|
+
const withoutHash = url.split("#")[0] ?? url;
|
|
1782
|
+
return withoutHash.split("?")[0] ?? withoutHash;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1504
1785
|
function createConsoleWideEventLogger(options = {}) {
|
|
1505
1786
|
const write = options.write ?? ((line) => {
|
|
1506
1787
|
console.info(line);
|
|
1507
1788
|
});
|
|
1508
1789
|
return (event) => {
|
|
1790
|
+
const redacted = redactEvent(event);
|
|
1509
1791
|
write(
|
|
1510
|
-
options.pretty ? JSON.stringify(
|
|
1792
|
+
options.pretty ? JSON.stringify(redacted, null, 2) : JSON.stringify(redacted)
|
|
1511
1793
|
);
|
|
1512
1794
|
};
|
|
1513
1795
|
}
|
|
1514
|
-
async function shouldEmitWideEvent(event, options = {}) {
|
|
1796
|
+
async function shouldEmitWideEvent(event, options = {}, random = Math.random) {
|
|
1515
1797
|
const sampleRate = options.sampleRate ?? 1;
|
|
1516
1798
|
if ((options.keepErrors ?? true) && event.outcome === "error") {
|
|
1517
1799
|
return true;
|
|
@@ -1545,7 +1827,7 @@ async function shouldEmitWideEvent(event, options = {}) {
|
|
|
1545
1827
|
if (sampleRate >= 1) {
|
|
1546
1828
|
return true;
|
|
1547
1829
|
}
|
|
1548
|
-
return
|
|
1830
|
+
return random() < sampleRate;
|
|
1549
1831
|
}
|
|
1550
1832
|
|
|
1551
1833
|
// src/router.ts
|
|
@@ -1610,6 +1892,19 @@ function buildWeightedOrder(routes) {
|
|
|
1610
1892
|
}
|
|
1611
1893
|
return expanded;
|
|
1612
1894
|
}
|
|
1895
|
+
var ROUTING_STRATEGIES = {
|
|
1896
|
+
priority: (ordered) => ordered,
|
|
1897
|
+
failover: (ordered) => ordered,
|
|
1898
|
+
"round-robin": (ordered, cursor, cursorKey) => {
|
|
1899
|
+
const index = cursor(cursorKey, ordered.length);
|
|
1900
|
+
return rotate(ordered, index);
|
|
1901
|
+
},
|
|
1902
|
+
"weighted-round-robin": (ordered, cursor, cursorKey) => {
|
|
1903
|
+
const weighted = buildWeightedOrder(ordered);
|
|
1904
|
+
const index = cursor(cursorKey, weighted.length);
|
|
1905
|
+
return unique(rotate(weighted, index));
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1613
1908
|
var RouteRegistry = class {
|
|
1614
1909
|
config;
|
|
1615
1910
|
runtime;
|
|
@@ -1680,14 +1975,21 @@ var RouteRegistry = class {
|
|
|
1680
1975
|
const routeIds = request.routeHints?.pool?.length ? resolveRouteSelectors(request.routeHints.pool, this.config.routes) : operationRouting.pool;
|
|
1681
1976
|
const excludedIds = new Set(request.routeHints?.excludeRouteIds ?? []);
|
|
1682
1977
|
const requestedModel = request.routeHints?.model;
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1978
|
+
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));
|
|
1979
|
+
const strategy = request.routeHints?.strategy ?? operationRouting.strategy;
|
|
1980
|
+
const cursorKey = `${operation}:${strategy}:${eligible.map((route) => route.id).join("|")}`;
|
|
1981
|
+
const healthyIds = new Set(
|
|
1982
|
+
eligible.filter((route) => {
|
|
1983
|
+
const state = this.getHealth(route).state;
|
|
1984
|
+
return state !== "cooling_down" && state !== "unavailable";
|
|
1985
|
+
}).map((route) => route.id)
|
|
1986
|
+
);
|
|
1687
1987
|
return this.#order(
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1988
|
+
strategy,
|
|
1989
|
+
eligible,
|
|
1990
|
+
healthyIds,
|
|
1991
|
+
request.routeHints?.preferredProviders,
|
|
1992
|
+
cursorKey
|
|
1691
1993
|
);
|
|
1692
1994
|
}
|
|
1693
1995
|
recordSuccess(route) {
|
|
@@ -1720,20 +2022,12 @@ var RouteRegistry = class {
|
|
|
1720
2022
|
failureCount
|
|
1721
2023
|
});
|
|
1722
2024
|
}
|
|
1723
|
-
#order(strategy,
|
|
1724
|
-
const sorted = baseSort(
|
|
1725
|
-
const
|
|
1726
|
-
const ordered =
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
if (strategy === "round-robin") {
|
|
1731
|
-
const index2 = this.#cursor(cursorKey, ordered.length);
|
|
1732
|
-
return rotate(ordered, index2);
|
|
1733
|
-
}
|
|
1734
|
-
const weighted = buildWeightedOrder(ordered);
|
|
1735
|
-
const index = this.#cursor(cursorKey, weighted.length);
|
|
1736
|
-
return unique(rotate(weighted, index));
|
|
2025
|
+
#order(strategy, eligible, healthyIds, preferredProviders, cursorKey) {
|
|
2026
|
+
const sorted = baseSort(eligible, preferredProviders);
|
|
2027
|
+
const orderedFull = this.config.routing.shuffleOnInit ? this.#shuffle(cursorKey, sorted) : sorted;
|
|
2028
|
+
const ordered = orderedFull.filter((route) => healthyIds.has(route.id));
|
|
2029
|
+
const strategyFn = ROUTING_STRATEGIES[strategy];
|
|
2030
|
+
return strategyFn(ordered, (key, length) => this.#cursor(key, length), cursorKey);
|
|
1737
2031
|
}
|
|
1738
2032
|
#shuffle(key, routes) {
|
|
1739
2033
|
const cached = this.#shuffles.get(key);
|
|
@@ -1837,8 +2131,22 @@ var MODEL_REFERENCE = {
|
|
|
1837
2131
|
"gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
|
|
1838
2132
|
"gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
|
|
1839
2133
|
};
|
|
2134
|
+
var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2135
|
+
"minimal",
|
|
2136
|
+
"low",
|
|
2137
|
+
"medium",
|
|
2138
|
+
"high",
|
|
2139
|
+
"xhigh"
|
|
2140
|
+
]);
|
|
1840
2141
|
function normalizeModelKey(model) {
|
|
1841
2142
|
let key = model.trim().toLowerCase();
|
|
2143
|
+
const lastSlashIndex = key.lastIndexOf("/");
|
|
2144
|
+
if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
|
|
2145
|
+
const tail = key.slice(lastSlashIndex + 1);
|
|
2146
|
+
if (REASONING_EFFORT_SUFFIXES.has(tail)) {
|
|
2147
|
+
key = key.slice(0, lastSlashIndex);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
1842
2150
|
const slashIndex = key.indexOf("/");
|
|
1843
2151
|
if (slashIndex > 0) {
|
|
1844
2152
|
key = key.slice(slashIndex + 1);
|
|
@@ -1846,6 +2154,7 @@ function normalizeModelKey(model) {
|
|
|
1846
2154
|
key = key.replace(/:(?:free|beta)$/i, "");
|
|
1847
2155
|
return key.trim();
|
|
1848
2156
|
}
|
|
2157
|
+
var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
|
|
1849
2158
|
function lookupModelRef(model) {
|
|
1850
2159
|
const normalized = normalizeModelKey(model);
|
|
1851
2160
|
if (!normalized) {
|
|
@@ -1868,7 +2177,7 @@ function lookupModelRef(model) {
|
|
|
1868
2177
|
}
|
|
1869
2178
|
let substringWinner;
|
|
1870
2179
|
for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
|
|
1871
|
-
if (normalized.includes(key)) {
|
|
2180
|
+
if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
|
|
1872
2181
|
if (!substringWinner || key.length > substringWinner.key.length) {
|
|
1873
2182
|
substringWinner = entry;
|
|
1874
2183
|
}
|
|
@@ -2103,6 +2412,14 @@ var DEFAULT_LOGGING_SERVICE = "ai-connect";
|
|
|
2103
2412
|
function splitCredentialValues(value, delimiter2) {
|
|
2104
2413
|
return value.split(delimiter2).map((item) => item.trim()).filter(Boolean);
|
|
2105
2414
|
}
|
|
2415
|
+
var ClientToolExecutionError = class extends Error {
|
|
2416
|
+
aiError;
|
|
2417
|
+
constructor(aiError) {
|
|
2418
|
+
super(aiError.message);
|
|
2419
|
+
this.name = "ClientToolExecutionError";
|
|
2420
|
+
this.aiError = aiError;
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2106
2423
|
function materializeRuntimeConfig(config, runtime) {
|
|
2107
2424
|
const routeExpansion = /* @__PURE__ */ new Map();
|
|
2108
2425
|
const routes = [];
|
|
@@ -2117,13 +2434,14 @@ function materializeRuntimeConfig(config, runtime) {
|
|
|
2117
2434
|
continue;
|
|
2118
2435
|
}
|
|
2119
2436
|
const expandedIds = [];
|
|
2437
|
+
const expandedWeight = Math.max(1, Math.floor(route.weight / values.length));
|
|
2120
2438
|
values.forEach((apiKey, index) => {
|
|
2121
2439
|
const credentialId = `${route.credentialId}#${index + 1}`;
|
|
2122
2440
|
const expandedRoute = {
|
|
2123
2441
|
...route,
|
|
2124
2442
|
id: `${route.id}#${index + 1}`,
|
|
2125
2443
|
credentialId,
|
|
2126
|
-
weight:
|
|
2444
|
+
weight: expandedWeight,
|
|
2127
2445
|
credential: {
|
|
2128
2446
|
...route.credential,
|
|
2129
2447
|
id: credentialId,
|
|
@@ -2164,6 +2482,27 @@ async function sleep(delayMs) {
|
|
|
2164
2482
|
}
|
|
2165
2483
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2166
2484
|
}
|
|
2485
|
+
async function runWithAbortRace(handlerPromise, abort) {
|
|
2486
|
+
if (abort.signal.aborted) {
|
|
2487
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2488
|
+
});
|
|
2489
|
+
throw mapAbortError(abort.reason);
|
|
2490
|
+
}
|
|
2491
|
+
let onAbort;
|
|
2492
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2493
|
+
onAbort = () => reject(mapAbortError(abort.reason));
|
|
2494
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2495
|
+
});
|
|
2496
|
+
try {
|
|
2497
|
+
return await Promise.race([handlerPromise, abortPromise]);
|
|
2498
|
+
} finally {
|
|
2499
|
+
if (onAbort) {
|
|
2500
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
2501
|
+
}
|
|
2502
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2167
2506
|
function normalizeWorkingDirectory(value) {
|
|
2168
2507
|
if (value === void 0) {
|
|
2169
2508
|
return void 0;
|
|
@@ -2205,6 +2544,12 @@ function normalizeClientToolRegistry(tools) {
|
|
|
2205
2544
|
const registry = /* @__PURE__ */ new Map();
|
|
2206
2545
|
for (const tool of tools ?? []) {
|
|
2207
2546
|
const normalized = normalizeClientToolDefinition(tool);
|
|
2547
|
+
if (registry.has(normalized.function.name)) {
|
|
2548
|
+
throw new AiConnectError(
|
|
2549
|
+
"validation_error",
|
|
2550
|
+
`Duplicate client tool name "${normalized.function.name}" registered on this client.`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2208
2553
|
registry.set(normalized.function.name, normalized);
|
|
2209
2554
|
}
|
|
2210
2555
|
return registry;
|
|
@@ -2230,10 +2575,22 @@ function resolveClientToolSelections(selections, registry) {
|
|
|
2230
2575
|
`No client tool named "${name}" is registered on this client.`
|
|
2231
2576
|
);
|
|
2232
2577
|
}
|
|
2578
|
+
if (resolved.has(name)) {
|
|
2579
|
+
throw new AiConnectError(
|
|
2580
|
+
"validation_error",
|
|
2581
|
+
`Duplicate client tool "${name}" in the clientTools selection.`
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2233
2584
|
resolved.set(name, registered);
|
|
2234
2585
|
continue;
|
|
2235
2586
|
}
|
|
2236
2587
|
const normalized = normalizeClientToolDefinition(selection);
|
|
2588
|
+
if (resolved.has(normalized.function.name)) {
|
|
2589
|
+
throw new AiConnectError(
|
|
2590
|
+
"validation_error",
|
|
2591
|
+
`Duplicate client tool "${normalized.function.name}" in the clientTools selection.`
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2237
2594
|
resolved.set(normalized.function.name, normalized);
|
|
2238
2595
|
}
|
|
2239
2596
|
return Array.from(resolved.values());
|
|
@@ -2346,6 +2703,34 @@ function requiresImageInput(request) {
|
|
|
2346
2703
|
function requiresFileUpload(request) {
|
|
2347
2704
|
return request.attachments.some((file) => isPortableDocumentFile(file));
|
|
2348
2705
|
}
|
|
2706
|
+
function resolveRequiredCapabilities(request) {
|
|
2707
|
+
const requiresSchema = requiresToolSchema(request);
|
|
2708
|
+
const requiresClientTools = requiresClientToolExecution(request);
|
|
2709
|
+
const requiresImage = requiresImageInput(request);
|
|
2710
|
+
const requiresFile = requiresFileUpload(request);
|
|
2711
|
+
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2712
|
+
const projected = requiresAnyCapability ? {
|
|
2713
|
+
...request,
|
|
2714
|
+
routeHints: {
|
|
2715
|
+
...request.routeHints ?? {},
|
|
2716
|
+
requiredCapabilities: {
|
|
2717
|
+
...request.routeHints?.requiredCapabilities ?? {},
|
|
2718
|
+
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2719
|
+
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2720
|
+
...requiresImage ? { supportsImageInput: true } : {},
|
|
2721
|
+
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
} : request;
|
|
2725
|
+
return {
|
|
2726
|
+
requiresSchema,
|
|
2727
|
+
requiresClientTools,
|
|
2728
|
+
requiresImage,
|
|
2729
|
+
requiresFile,
|
|
2730
|
+
requiresAnyCapability,
|
|
2731
|
+
request: projected
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2349
2734
|
function normalizeRequestBase(request) {
|
|
2350
2735
|
if (request.messages.length === 0) {
|
|
2351
2736
|
throw new AiConnectError("validation_error", "Unified generate/stream requests must include at least one message.");
|
|
@@ -2554,7 +2939,10 @@ function summarizeRoute(route) {
|
|
|
2554
2939
|
model: route.model,
|
|
2555
2940
|
handlerKey: route.handlerKey,
|
|
2556
2941
|
profileId: route.profileId,
|
|
2557
|
-
|
|
2942
|
+
// SYS-3 / C1a: never emit a raw baseUrl into a wide event — a token embedded in
|
|
2943
|
+
// userinfo (`https://user:secret@host`) or the query string would leak verbatim,
|
|
2944
|
+
// unlike the secret-safe toPublicRoute projection. Strip credentials first.
|
|
2945
|
+
...route.transport.baseUrl ? { baseUrl: redactBaseUrl(route.transport.baseUrl) } : {}
|
|
2558
2946
|
};
|
|
2559
2947
|
}
|
|
2560
2948
|
function summarizeFiles(files) {
|
|
@@ -2865,21 +3253,40 @@ function createClient(configOrInput, options = {}) {
|
|
|
2865
3253
|
for (const toolCall of toolCalls) {
|
|
2866
3254
|
const tool = registry.get(toolCall.name);
|
|
2867
3255
|
if (!tool) {
|
|
2868
|
-
throw new
|
|
2869
|
-
|
|
2870
|
-
|
|
3256
|
+
throw new ClientToolExecutionError(
|
|
3257
|
+
new AiConnectError(
|
|
3258
|
+
"validation_error",
|
|
3259
|
+
`No client tool handler is registered for "${toolCall.name}".`
|
|
3260
|
+
)
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
if (abort.signal.aborted) {
|
|
3264
|
+
throw mapAbortError(abort.reason);
|
|
3265
|
+
}
|
|
3266
|
+
let execution;
|
|
3267
|
+
try {
|
|
3268
|
+
execution = normalizeToolResult(
|
|
3269
|
+
await tool.execute(toolCall.arguments, {
|
|
3270
|
+
route,
|
|
3271
|
+
request,
|
|
3272
|
+
runtime,
|
|
3273
|
+
toolCall,
|
|
3274
|
+
messages,
|
|
3275
|
+
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
3276
|
+
})
|
|
3277
|
+
);
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
if (abort.signal.aborted) {
|
|
3280
|
+
throw mapAbortError(abort.reason);
|
|
3281
|
+
}
|
|
3282
|
+
throw new ClientToolExecutionError(
|
|
3283
|
+
new AiConnectError(
|
|
3284
|
+
"validation_error",
|
|
3285
|
+
`Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
3286
|
+
{ cause: error }
|
|
3287
|
+
)
|
|
2871
3288
|
);
|
|
2872
3289
|
}
|
|
2873
|
-
const execution = normalizeToolResult(
|
|
2874
|
-
await tool.execute(toolCall.arguments, {
|
|
2875
|
-
route,
|
|
2876
|
-
request,
|
|
2877
|
-
runtime,
|
|
2878
|
-
toolCall,
|
|
2879
|
-
messages,
|
|
2880
|
-
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
2881
|
-
})
|
|
2882
|
-
);
|
|
2883
3290
|
toolMessages.push({
|
|
2884
3291
|
role: "tool",
|
|
2885
3292
|
content: execution.content,
|
|
@@ -2893,28 +3300,33 @@ function createClient(configOrInput, options = {}) {
|
|
|
2893
3300
|
if (abort.signal.aborted) {
|
|
2894
3301
|
throw mapAbortError(abort.reason);
|
|
2895
3302
|
}
|
|
2896
|
-
output = await
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3303
|
+
output = await runWithAbortRace(
|
|
3304
|
+
handler({
|
|
3305
|
+
route,
|
|
3306
|
+
request: {
|
|
3307
|
+
...request,
|
|
3308
|
+
messages,
|
|
3309
|
+
attachments: []
|
|
3310
|
+
},
|
|
3311
|
+
runtime,
|
|
3312
|
+
attempt: baseAttempt,
|
|
3313
|
+
abort,
|
|
3314
|
+
telemetry: {
|
|
3315
|
+
captureTransport(summary) {
|
|
3316
|
+
transportSummary2 = summary;
|
|
3317
|
+
}
|
|
2909
3318
|
}
|
|
2910
|
-
}
|
|
2911
|
-
|
|
3319
|
+
}),
|
|
3320
|
+
abort
|
|
3321
|
+
);
|
|
2912
3322
|
warnings.push(...output.warnings ?? []);
|
|
2913
3323
|
usage = mergeUsage(usage, output.usage);
|
|
2914
3324
|
}
|
|
2915
|
-
throw new
|
|
2916
|
-
|
|
2917
|
-
|
|
3325
|
+
throw new ClientToolExecutionError(
|
|
3326
|
+
new AiConnectError(
|
|
3327
|
+
"validation_error",
|
|
3328
|
+
"clientTools exceeded the maximum tool-call loop depth of 8."
|
|
3329
|
+
)
|
|
2918
3330
|
);
|
|
2919
3331
|
}
|
|
2920
3332
|
async function emitWideEvent(event) {
|
|
@@ -2945,26 +3357,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
2945
3357
|
async function executeGenerateOperation(normalizedRequest, operationName, abort, seed = {}) {
|
|
2946
3358
|
const startedAt = seed.startMs ?? nowMs(runtime);
|
|
2947
3359
|
const requestId = seed.requestId ?? resolveEventRequestId(normalizedRequest.logContext);
|
|
2948
|
-
const
|
|
2949
|
-
const
|
|
2950
|
-
const requiresImage = requiresImageInput(normalizedRequest);
|
|
2951
|
-
const requiresFile = requiresFileUpload(normalizedRequest);
|
|
2952
|
-
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2953
|
-
const resolvedCandidates = router.resolveCandidates(
|
|
2954
|
-
requiresAnyCapability ? {
|
|
2955
|
-
...normalizedRequest,
|
|
2956
|
-
routeHints: {
|
|
2957
|
-
...normalizedRequest.routeHints ?? {},
|
|
2958
|
-
requiredCapabilities: {
|
|
2959
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
2960
|
-
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2961
|
-
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2962
|
-
...requiresImage ? { supportsImageInput: true } : {},
|
|
2963
|
-
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
} : normalizedRequest
|
|
2967
|
-
);
|
|
3360
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
3361
|
+
const resolvedCandidates = router.resolveCandidates(capabilityScopedRequest);
|
|
2968
3362
|
const candidates = seed.candidates ?? resolvedCandidates.map(summarizeRoute);
|
|
2969
3363
|
const requestSummary = summarizeRequest(normalizedRequest, candidates);
|
|
2970
3364
|
if (resolvedCandidates.length === 0) {
|
|
@@ -3052,18 +3446,21 @@ function createClient(configOrInput, options = {}) {
|
|
|
3052
3446
|
const attemptStartedAt = nowMs(runtime);
|
|
3053
3447
|
let transportSummary2;
|
|
3054
3448
|
try {
|
|
3055
|
-
let output = await
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3449
|
+
let output = await runWithAbortRace(
|
|
3450
|
+
handler.generate({
|
|
3451
|
+
route,
|
|
3452
|
+
request: routeRequest,
|
|
3453
|
+
runtime,
|
|
3454
|
+
attempt: attempts.length + 1,
|
|
3455
|
+
abort,
|
|
3456
|
+
telemetry: {
|
|
3457
|
+
captureTransport(summary) {
|
|
3458
|
+
transportSummary2 = summary;
|
|
3459
|
+
}
|
|
3064
3460
|
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3461
|
+
}),
|
|
3462
|
+
abort
|
|
3463
|
+
);
|
|
3067
3464
|
if (routeRequest.clientTools?.length && output.toolCalls?.length) {
|
|
3068
3465
|
const loopResult = await executeClientToolLoop(
|
|
3069
3466
|
route,
|
|
@@ -3078,6 +3475,9 @@ function createClient(configOrInput, options = {}) {
|
|
|
3078
3475
|
transportSummary2 = loopResult.transportSummary;
|
|
3079
3476
|
}
|
|
3080
3477
|
}
|
|
3478
|
+
if (abort.signal.aborted) {
|
|
3479
|
+
throw mapAbortError(abort.reason);
|
|
3480
|
+
}
|
|
3081
3481
|
wideAttempts.push({
|
|
3082
3482
|
index: wideAttempts.length + 1,
|
|
3083
3483
|
route: routeSummary,
|
|
@@ -3148,6 +3548,49 @@ function createClient(configOrInput, options = {}) {
|
|
|
3148
3548
|
routeId: route.id
|
|
3149
3549
|
});
|
|
3150
3550
|
}
|
|
3551
|
+
if (error instanceof ClientToolExecutionError) {
|
|
3552
|
+
const toolError = error.aiError;
|
|
3553
|
+
attempts.push({
|
|
3554
|
+
routeId: route.id,
|
|
3555
|
+
handlerKey: route.handlerKey,
|
|
3556
|
+
errorCode: toolError.code,
|
|
3557
|
+
message: toolError.message
|
|
3558
|
+
});
|
|
3559
|
+
wideAttempts.push({
|
|
3560
|
+
index: wideAttempts.length + 1,
|
|
3561
|
+
route: routeSummary,
|
|
3562
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
3563
|
+
outcome: "error",
|
|
3564
|
+
retryCount,
|
|
3565
|
+
errorCode: toolError.code,
|
|
3566
|
+
message: toolError.message,
|
|
3567
|
+
...transportSummary2 ? { transport: transportSummary2 } : {}
|
|
3568
|
+
});
|
|
3569
|
+
await emitWideEvent({
|
|
3570
|
+
timestamp: isoTimestamp(startedAt),
|
|
3571
|
+
event: "ai_connect.operation",
|
|
3572
|
+
requestId,
|
|
3573
|
+
operationName,
|
|
3574
|
+
operation: normalizedRequest.operation,
|
|
3575
|
+
runtime: runtime.kind,
|
|
3576
|
+
outcome: "error",
|
|
3577
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
3578
|
+
candidates,
|
|
3579
|
+
attempts: wideAttempts,
|
|
3580
|
+
request: requestSummary,
|
|
3581
|
+
error: {
|
|
3582
|
+
code: toolError.code,
|
|
3583
|
+
message: toolError.message,
|
|
3584
|
+
routeId: route.id
|
|
3585
|
+
},
|
|
3586
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
3587
|
+
});
|
|
3588
|
+
throw new AiConnectError(toolError.code, toolError.message, {
|
|
3589
|
+
...toolError.details,
|
|
3590
|
+
attempts,
|
|
3591
|
+
routeId: route.id
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3151
3594
|
const normalizedError = toAiConnectError(error);
|
|
3152
3595
|
const actions = config.routing.fallback.on[normalizedError.code] ?? [];
|
|
3153
3596
|
attempts.push({
|
|
@@ -3392,30 +3835,16 @@ function createClient(configOrInput, options = {}) {
|
|
|
3392
3835
|
return report;
|
|
3393
3836
|
}
|
|
3394
3837
|
async function runPingWithAbort(route, handler, abort) {
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3838
|
+
return runWithAbortRace(
|
|
3839
|
+
handler({
|
|
3840
|
+
route,
|
|
3841
|
+
request: buildPingRequest(),
|
|
3842
|
+
runtime,
|
|
3843
|
+
attempt: 1,
|
|
3844
|
+
abort
|
|
3845
|
+
}),
|
|
3403
3846
|
abort
|
|
3404
|
-
|
|
3405
|
-
let onAbort;
|
|
3406
|
-
const abortPromise = new Promise((_, reject) => {
|
|
3407
|
-
onAbort = () => reject(mapAbortError(abort.reason));
|
|
3408
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
3409
|
-
});
|
|
3410
|
-
try {
|
|
3411
|
-
return await Promise.race([handlerPromise, abortPromise]);
|
|
3412
|
-
} finally {
|
|
3413
|
-
if (onAbort) {
|
|
3414
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
3415
|
-
}
|
|
3416
|
-
void Promise.resolve(handlerPromise).catch(() => {
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3847
|
+
);
|
|
3419
3848
|
}
|
|
3420
3849
|
async function checkEndpointReachable(route, handler, abort) {
|
|
3421
3850
|
if (abort.signal.aborted) {
|
|
@@ -3983,19 +4412,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
3983
4412
|
release = await limiter.acquire(abort.signal);
|
|
3984
4413
|
const selectedModel = await resolveSelectedModel(preparedRequest);
|
|
3985
4414
|
const normalizedRequest = withSelectedModel(preparedRequest, selectedModel);
|
|
3986
|
-
const
|
|
3987
|
-
const candidates = router.resolveCandidates(
|
|
3988
|
-
requiresSchema ? {
|
|
3989
|
-
...normalizedRequest,
|
|
3990
|
-
routeHints: {
|
|
3991
|
-
...normalizedRequest.routeHints ?? {},
|
|
3992
|
-
requiredCapabilities: {
|
|
3993
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
3994
|
-
supportsToolSchema: true
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
} : normalizedRequest
|
|
3998
|
-
);
|
|
4415
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
4416
|
+
const candidates = router.resolveCandidates(capabilityScopedRequest);
|
|
3999
4417
|
const startedAt = nowMs(runtime);
|
|
4000
4418
|
const requestId = resolveEventRequestId(normalizedRequest.logContext);
|
|
4001
4419
|
const candidateSummaries = candidates.map(summarizeRoute);
|
|
@@ -4003,7 +4421,10 @@ function createClient(configOrInput, options = {}) {
|
|
|
4003
4421
|
const attempts = [];
|
|
4004
4422
|
const wideAttempts = [];
|
|
4005
4423
|
if (candidates.length === 0) {
|
|
4006
|
-
const error =
|
|
4424
|
+
const error = requiresImage || requiresFile ? new AiConnectError(
|
|
4425
|
+
"unsupported_capability",
|
|
4426
|
+
`No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
|
|
4427
|
+
) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
|
|
4007
4428
|
await emitWideEvent({
|
|
4008
4429
|
timestamp: isoTimestamp(startedAt),
|
|
4009
4430
|
event: "ai_connect.operation",
|
|
@@ -4479,12 +4900,32 @@ function trimTrailingSlash(value) {
|
|
|
4479
4900
|
function appendPath(baseUrl, suffix) {
|
|
4480
4901
|
return `${trimTrailingSlash(baseUrl)}/${suffix.replace(/^\/+/, "")}`;
|
|
4481
4902
|
}
|
|
4903
|
+
function isAbortError(error) {
|
|
4904
|
+
if (error instanceof AiConnectError) {
|
|
4905
|
+
return error.code === "aborted";
|
|
4906
|
+
}
|
|
4907
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
4908
|
+
}
|
|
4909
|
+
function attachPartsToLastUserTurn(conversation, createEmptyUserTurn) {
|
|
4910
|
+
let targetIndex = -1;
|
|
4911
|
+
for (let index = conversation.length - 1; index >= 0; index -= 1) {
|
|
4912
|
+
if (conversation[index]?.role === "user") {
|
|
4913
|
+
targetIndex = index;
|
|
4914
|
+
break;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
if (targetIndex === -1) {
|
|
4918
|
+
conversation.push(createEmptyUserTurn());
|
|
4919
|
+
targetIndex = conversation.length - 1;
|
|
4920
|
+
}
|
|
4921
|
+
return conversation[targetIndex];
|
|
4922
|
+
}
|
|
4482
4923
|
function resolveOpenAiEndpoint(route, operation) {
|
|
4483
4924
|
const configured = route.transport.baseUrl?.trim();
|
|
4484
4925
|
if (!configured) {
|
|
4485
4926
|
return operation === "image" ? "https://api.openai.com/v1/images/generations" : "https://api.openai.com/v1/chat/completions";
|
|
4486
4927
|
}
|
|
4487
|
-
if (configured.endsWith("/chat/completions") || configured.endsWith("/images/generations")) {
|
|
4928
|
+
if (operation === "text" && configured.endsWith("/chat/completions") || operation === "image" && configured.endsWith("/images/generations")) {
|
|
4488
4929
|
return configured;
|
|
4489
4930
|
}
|
|
4490
4931
|
return appendPath(
|
|
@@ -5315,7 +5756,7 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5315
5756
|
}
|
|
5316
5757
|
try {
|
|
5317
5758
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5318
|
-
const root = base ? base.replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5759
|
+
const root = base ? trimTrailingSlash(base).replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5319
5760
|
const endpoint = appendPath(root, "/files");
|
|
5320
5761
|
const form = new FormData();
|
|
5321
5762
|
form.append(
|
|
@@ -5339,7 +5780,10 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5339
5780
|
}
|
|
5340
5781
|
const id = recordOf(data).id;
|
|
5341
5782
|
return typeof id === "string" ? id : void 0;
|
|
5342
|
-
} catch {
|
|
5783
|
+
} catch (error) {
|
|
5784
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5785
|
+
throw error;
|
|
5786
|
+
}
|
|
5343
5787
|
return void 0;
|
|
5344
5788
|
}
|
|
5345
5789
|
}
|
|
@@ -5372,7 +5816,10 @@ async function uploadOpenAiFile(payload, uploader) {
|
|
|
5372
5816
|
}
|
|
5373
5817
|
const id = recordOf(data).id;
|
|
5374
5818
|
return typeof id === "string" ? id : void 0;
|
|
5375
|
-
} catch {
|
|
5819
|
+
} catch (error) {
|
|
5820
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5821
|
+
throw error;
|
|
5822
|
+
}
|
|
5376
5823
|
return void 0;
|
|
5377
5824
|
}
|
|
5378
5825
|
}
|
|
@@ -5382,10 +5829,11 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5382
5829
|
}
|
|
5383
5830
|
try {
|
|
5384
5831
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5385
|
-
const
|
|
5386
|
-
|
|
5832
|
+
const root = base ? trimTrailingSlash(base.replace(/\/v1beta\/models.*$/, "/v1beta")) : void 0;
|
|
5833
|
+
const endpoint = root ? appendPath(
|
|
5834
|
+
root.endsWith("/v1beta") ? `${root.slice(0, -"/v1beta".length)}/upload/v1beta` : `${root}/upload`,
|
|
5387
5835
|
"/files"
|
|
5388
|
-
)
|
|
5836
|
+
) : "https://generativelanguage.googleapis.com/upload/v1beta/files";
|
|
5389
5837
|
const response = await uploader.fetchImpl(endpoint, {
|
|
5390
5838
|
method: "POST",
|
|
5391
5839
|
headers: {
|
|
@@ -5404,7 +5852,10 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5404
5852
|
const file = recordOf(recordOf(data).file);
|
|
5405
5853
|
const uri = file.uri ?? recordOf(data).uri;
|
|
5406
5854
|
return typeof uri === "string" ? uri : void 0;
|
|
5407
|
-
} catch {
|
|
5855
|
+
} catch (error) {
|
|
5856
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5857
|
+
throw error;
|
|
5858
|
+
}
|
|
5408
5859
|
return void 0;
|
|
5409
5860
|
}
|
|
5410
5861
|
}
|
|
@@ -5602,21 +6053,10 @@ function attachOpenAiParts(conversation, parts) {
|
|
|
5602
6053
|
if (parts.length === 0) {
|
|
5603
6054
|
return conversation;
|
|
5604
6055
|
}
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
break;
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
if (targetIndex === -1) {
|
|
5613
|
-
conversation.push({
|
|
5614
|
-
role: "user",
|
|
5615
|
-
content: ""
|
|
5616
|
-
});
|
|
5617
|
-
targetIndex = conversation.length - 1;
|
|
5618
|
-
}
|
|
5619
|
-
const target = conversation[targetIndex];
|
|
6056
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6057
|
+
role: "user",
|
|
6058
|
+
content: ""
|
|
6059
|
+
}));
|
|
5620
6060
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5621
6061
|
target.content = [...baseParts, ...parts];
|
|
5622
6062
|
return conversation;
|
|
@@ -5702,21 +6142,10 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5702
6142
|
}
|
|
5703
6143
|
const attachmentParts = await anthropicAttachmentParts(attachments, uploader);
|
|
5704
6144
|
if (attachmentParts.length > 0) {
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
break;
|
|
5710
|
-
}
|
|
5711
|
-
}
|
|
5712
|
-
if (targetIndex === -1) {
|
|
5713
|
-
conversation.push({
|
|
5714
|
-
role: "user",
|
|
5715
|
-
content: ""
|
|
5716
|
-
});
|
|
5717
|
-
targetIndex = conversation.length - 1;
|
|
5718
|
-
}
|
|
5719
|
-
const target = conversation[targetIndex];
|
|
6145
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6146
|
+
role: "user",
|
|
6147
|
+
content: ""
|
|
6148
|
+
}));
|
|
5720
6149
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5721
6150
|
target.content = [...baseParts, ...attachmentParts];
|
|
5722
6151
|
}
|
|
@@ -5727,12 +6156,27 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5727
6156
|
}
|
|
5728
6157
|
async function geminiRequest(messages, attachments, uploader) {
|
|
5729
6158
|
const system = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n");
|
|
6159
|
+
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
6160
|
+
for (const message of messages) {
|
|
6161
|
+
if (hasAssistantToolCalls(message)) {
|
|
6162
|
+
for (const call of message.toolCalls) {
|
|
6163
|
+
toolNameByCallId.set(call.id, call.name);
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
5730
6167
|
const contents = messages.filter((message) => message.role !== "system").reduce(
|
|
5731
6168
|
(conversation, message) => {
|
|
5732
6169
|
if (isToolMessage(message)) {
|
|
6170
|
+
const resolvedName = message.toolName ?? toolNameByCallId.get(message.toolCallId);
|
|
6171
|
+
if (!resolvedName) {
|
|
6172
|
+
throw new AiConnectError(
|
|
6173
|
+
"validation_error",
|
|
6174
|
+
`Gemini functionResponse requires a tool name: the tool message for toolCallId "${message.toolCallId}" has no toolName and no matching assistant toolCall was found.`
|
|
6175
|
+
);
|
|
6176
|
+
}
|
|
5733
6177
|
const response = {
|
|
5734
6178
|
functionResponse: {
|
|
5735
|
-
name:
|
|
6179
|
+
name: resolvedName,
|
|
5736
6180
|
response: isRecord(message.data) ? message.data : {
|
|
5737
6181
|
content: message.content,
|
|
5738
6182
|
...message.isError !== void 0 ? { isError: message.isError } : {}
|
|
@@ -5779,21 +6223,11 @@ async function geminiRequest(messages, attachments, uploader) {
|
|
|
5779
6223
|
);
|
|
5780
6224
|
const attachmentParts = await geminiAttachmentParts(attachments, uploader);
|
|
5781
6225
|
if (attachmentParts.length > 0) {
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
}
|
|
5788
|
-
}
|
|
5789
|
-
if (targetIndex === -1) {
|
|
5790
|
-
contents.push({
|
|
5791
|
-
role: "user",
|
|
5792
|
-
parts: []
|
|
5793
|
-
});
|
|
5794
|
-
targetIndex = contents.length - 1;
|
|
5795
|
-
}
|
|
5796
|
-
contents[targetIndex].parts.push(...attachmentParts);
|
|
6226
|
+
const target = attachPartsToLastUserTurn(contents, () => ({
|
|
6227
|
+
role: "user",
|
|
6228
|
+
parts: []
|
|
6229
|
+
}));
|
|
6230
|
+
target.parts.push(...attachmentParts);
|
|
5797
6231
|
}
|
|
5798
6232
|
return {
|
|
5799
6233
|
...system ? {
|
|
@@ -5840,7 +6274,23 @@ function extractThoughts(value) {
|
|
|
5840
6274
|
}
|
|
5841
6275
|
return [];
|
|
5842
6276
|
});
|
|
5843
|
-
const joined = thoughts.join("\n").trim();
|
|
6277
|
+
const joined = thoughts.join("\n").trim();
|
|
6278
|
+
return joined.length > 0 ? joined : void 0;
|
|
6279
|
+
}
|
|
6280
|
+
function extractThinking(value) {
|
|
6281
|
+
if (!Array.isArray(value)) {
|
|
6282
|
+
return void 0;
|
|
6283
|
+
}
|
|
6284
|
+
const thinking = value.flatMap((part) => {
|
|
6285
|
+
if (part && typeof part === "object" && part.type === "thinking") {
|
|
6286
|
+
const candidate = part.thinking;
|
|
6287
|
+
if (typeof candidate === "string") {
|
|
6288
|
+
return [candidate];
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
return [];
|
|
6292
|
+
});
|
|
6293
|
+
const joined = thinking.join("\n").trim();
|
|
5844
6294
|
return joined.length > 0 ? joined : void 0;
|
|
5845
6295
|
}
|
|
5846
6296
|
function imageAttachmentsFromPayload(payload, ...sources) {
|
|
@@ -5930,6 +6380,10 @@ function buildOpenAiChatBody(context, messages, parameters, responseFormat, opts
|
|
|
5930
6380
|
model: context.route.model,
|
|
5931
6381
|
messages,
|
|
5932
6382
|
stream: opts.stream,
|
|
6383
|
+
// Ask OpenAI-compatible streaming routes for the trailing usage chunk; without
|
|
6384
|
+
// `stream_options.include_usage` the SSE stream carries no `usage` block and
|
|
6385
|
+
// `runOpenAiStream` would always report `usage: undefined` (H5).
|
|
6386
|
+
...opts.stream ? { stream_options: { include_usage: true } } : {},
|
|
5933
6387
|
...parameters?.maxTokens !== void 0 ? { max_tokens: parameters.maxTokens } : {},
|
|
5934
6388
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
5935
6389
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6084,6 +6538,11 @@ async function* runOpenAiStream(fetchImpl, context) {
|
|
|
6084
6538
|
}
|
|
6085
6539
|
const response = outcome.response;
|
|
6086
6540
|
const streamWarnings = [...uploaderWarnings];
|
|
6541
|
+
if (parameters?.candidateCount && parameters.candidateCount > 1) {
|
|
6542
|
+
streamWarnings.push(
|
|
6543
|
+
"candidateCount > 1 is forwarded to OpenAI-compatible text routes, but the normalized result returns only the first choice."
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6087
6546
|
if (droppedResponseFormat) {
|
|
6088
6547
|
streamWarnings.push(
|
|
6089
6548
|
"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."
|
|
@@ -6229,8 +6688,7 @@ function buildOpenAiStreamResult(context, text, toolCalls, usage, warnings) {
|
|
|
6229
6688
|
} : {}
|
|
6230
6689
|
};
|
|
6231
6690
|
}
|
|
6232
|
-
async function
|
|
6233
|
-
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6691
|
+
async function runProviderImage(fetchImpl, context, opts) {
|
|
6234
6692
|
const referenceImages = imageReferenceFiles(context);
|
|
6235
6693
|
const bundle = buildImagePromptBundle({
|
|
6236
6694
|
request: context.request,
|
|
@@ -6239,10 +6697,10 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6239
6697
|
});
|
|
6240
6698
|
const baseFields = buildImageRequestFields(context, bundle);
|
|
6241
6699
|
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6242
|
-
const endpoint = useEditEndpoint ?
|
|
6700
|
+
const endpoint = useEditEndpoint ? opts.resolveEditEndpoint(context.route) : opts.resolveEndpoint(context.route);
|
|
6243
6701
|
transportSummary(
|
|
6244
6702
|
context,
|
|
6245
|
-
useEditEndpoint ?
|
|
6703
|
+
useEditEndpoint ? `${opts.providerLabel}-image-edit` : `${opts.providerLabel}-image`,
|
|
6246
6704
|
endpoint,
|
|
6247
6705
|
"POST",
|
|
6248
6706
|
false,
|
|
@@ -6255,23 +6713,21 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6255
6713
|
);
|
|
6256
6714
|
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6257
6715
|
method: "POST",
|
|
6258
|
-
headers: {
|
|
6259
|
-
authorization: `Bearer ${apiKey}`
|
|
6260
|
-
},
|
|
6716
|
+
headers: { ...opts.authHeaders },
|
|
6261
6717
|
body: await buildImageEditFormData(context, baseFields),
|
|
6262
6718
|
signal: context.abort.signal
|
|
6263
6719
|
} : {
|
|
6264
6720
|
method: "POST",
|
|
6265
6721
|
headers: {
|
|
6266
6722
|
"content-type": "application/json",
|
|
6267
|
-
|
|
6723
|
+
...opts.authHeaders
|
|
6268
6724
|
},
|
|
6269
6725
|
body: JSON.stringify(baseFields),
|
|
6270
6726
|
signal: context.abort.signal
|
|
6271
6727
|
});
|
|
6272
6728
|
const payload = await readPayload(response);
|
|
6273
6729
|
if (!response.ok) {
|
|
6274
|
-
throw classifyApiError(
|
|
6730
|
+
throw classifyApiError(opts.classifyProvider, response, payload);
|
|
6275
6731
|
}
|
|
6276
6732
|
return {
|
|
6277
6733
|
data: {
|
|
@@ -6281,6 +6737,16 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6281
6737
|
attachments: imageAttachmentsFromPayload(payload)
|
|
6282
6738
|
};
|
|
6283
6739
|
}
|
|
6740
|
+
async function runOpenAiImage(fetchImpl, context) {
|
|
6741
|
+
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6742
|
+
return runProviderImage(fetchImpl, context, {
|
|
6743
|
+
resolveEndpoint: (route) => resolveOpenAiEndpoint(route, "image"),
|
|
6744
|
+
resolveEditEndpoint: (route) => resolveOpenAiImageEditEndpoint(route),
|
|
6745
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6746
|
+
providerLabel: "openai",
|
|
6747
|
+
classifyProvider: "openai"
|
|
6748
|
+
});
|
|
6749
|
+
}
|
|
6284
6750
|
async function runAnthropic(fetchImpl, context) {
|
|
6285
6751
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6286
6752
|
const uploaderWarnings = [];
|
|
@@ -6292,6 +6758,8 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6292
6758
|
const parameters = context.request.parameters;
|
|
6293
6759
|
const requestBody = {
|
|
6294
6760
|
model: context.route.model,
|
|
6761
|
+
// Anthropic REQUIRES `max_tokens` (unlike OpenAI/Gemini, where it is optional), so a
|
|
6762
|
+
// provider-specific default of 4096 is applied when the caller omits `maxTokens`.
|
|
6295
6763
|
max_tokens: parameters?.maxTokens ?? 4096,
|
|
6296
6764
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
6297
6765
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6321,8 +6789,12 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6321
6789
|
throw classifyApiError("anthropic", response, payload);
|
|
6322
6790
|
}
|
|
6323
6791
|
const data = payload;
|
|
6792
|
+
const anthropicReasoning = extractThinking(data.content);
|
|
6324
6793
|
return {
|
|
6325
6794
|
text: extractText(data.content),
|
|
6795
|
+
// Anthropic extended-thinking blocks, separated from the answer (consumers route this
|
|
6796
|
+
// to a thinking UI), mirroring the Gemini `reasoning` path.
|
|
6797
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
|
|
6326
6798
|
data,
|
|
6327
6799
|
...anthropicToolCallsFromPayload(data).length > 0 ? { toolCalls: anthropicToolCallsFromPayload(data) } : {},
|
|
6328
6800
|
...uploaderWarnings.length > 0 ? { warnings: uploaderWarnings } : {},
|
|
@@ -6417,55 +6889,13 @@ async function runGemini(fetchImpl, context) {
|
|
|
6417
6889
|
}
|
|
6418
6890
|
async function runGeminiImage(fetchImpl, context) {
|
|
6419
6891
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
const baseFields = buildImageRequestFields(context, bundle);
|
|
6427
|
-
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6428
|
-
const endpoint = useEditEndpoint ? resolveGeminiImageEditEndpoint(context.route) : resolveGeminiImageEndpoint(context.route);
|
|
6429
|
-
transportSummary(
|
|
6430
|
-
context,
|
|
6431
|
-
useEditEndpoint ? "gemini-image-edit" : "gemini-image",
|
|
6432
|
-
endpoint,
|
|
6433
|
-
"POST",
|
|
6434
|
-
false,
|
|
6435
|
-
{
|
|
6436
|
-
...baseFields,
|
|
6437
|
-
source_image: Boolean(context.request.image?.sourceImage),
|
|
6438
|
-
mask: Boolean(context.request.image?.mask),
|
|
6439
|
-
reference_image_count: referenceImages.length
|
|
6440
|
-
}
|
|
6441
|
-
);
|
|
6442
|
-
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6443
|
-
method: "POST",
|
|
6444
|
-
headers: {
|
|
6445
|
-
authorization: `Bearer ${apiKey}`
|
|
6446
|
-
},
|
|
6447
|
-
body: await buildImageEditFormData(context, baseFields),
|
|
6448
|
-
signal: context.abort.signal
|
|
6449
|
-
} : {
|
|
6450
|
-
method: "POST",
|
|
6451
|
-
headers: {
|
|
6452
|
-
"content-type": "application/json",
|
|
6453
|
-
authorization: `Bearer ${apiKey}`
|
|
6454
|
-
},
|
|
6455
|
-
body: JSON.stringify(baseFields),
|
|
6456
|
-
signal: context.abort.signal
|
|
6892
|
+
return runProviderImage(fetchImpl, context, {
|
|
6893
|
+
resolveEndpoint: (route) => resolveGeminiImageEndpoint(route),
|
|
6894
|
+
resolveEditEndpoint: (route) => resolveGeminiImageEditEndpoint(route),
|
|
6895
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6896
|
+
providerLabel: "gemini",
|
|
6897
|
+
classifyProvider: context.route.provider
|
|
6457
6898
|
});
|
|
6458
|
-
const payload = await readPayload(response);
|
|
6459
|
-
if (!response.ok) {
|
|
6460
|
-
throw classifyApiError(context.route.provider, response, payload);
|
|
6461
|
-
}
|
|
6462
|
-
return {
|
|
6463
|
-
data: {
|
|
6464
|
-
request: bundle,
|
|
6465
|
-
response: payload
|
|
6466
|
-
},
|
|
6467
|
-
attachments: imageAttachmentsFromPayload(payload)
|
|
6468
|
-
};
|
|
6469
6899
|
}
|
|
6470
6900
|
function verifyApiRoute(route, runtime) {
|
|
6471
6901
|
resolveApiKey(route, runtime);
|
|
@@ -6544,9 +6974,12 @@ var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
|
|
|
6544
6974
|
transportId: "opencode-server"
|
|
6545
6975
|
}
|
|
6546
6976
|
};
|
|
6547
|
-
var AI_CONNECT_DEFAULT_SERVER_COMMANDS =
|
|
6548
|
-
|
|
6549
|
-
}
|
|
6977
|
+
var AI_CONNECT_DEFAULT_SERVER_COMMANDS = Object.fromEntries(
|
|
6978
|
+
Object.values(AI_CONNECT_DEFAULT_SERVER_PRESETS).map((preset) => [
|
|
6979
|
+
`${preset.id}:${preset.transportId}`,
|
|
6980
|
+
preset.command
|
|
6981
|
+
])
|
|
6982
|
+
);
|
|
6550
6983
|
|
|
6551
6984
|
// src/catalog.ts
|
|
6552
6985
|
var TEXT_PROVIDER_CATALOG = [
|
|
@@ -6763,8 +7196,119 @@ import fs from "node:fs/promises";
|
|
|
6763
7196
|
import os from "node:os";
|
|
6764
7197
|
import path from "node:path";
|
|
6765
7198
|
import { pathToFileURL } from "node:url";
|
|
7199
|
+
|
|
7200
|
+
// src/transport-runtime.ts
|
|
7201
|
+
var MINIMAL_ENV_KEYS = [
|
|
7202
|
+
"PATH",
|
|
7203
|
+
"HOME",
|
|
7204
|
+
"USERPROFILE",
|
|
7205
|
+
"HTTPS_PROXY",
|
|
7206
|
+
"HTTP_PROXY",
|
|
7207
|
+
"NO_PROXY",
|
|
7208
|
+
"https_proxy",
|
|
7209
|
+
"http_proxy",
|
|
7210
|
+
"no_proxy",
|
|
7211
|
+
"NODE_EXTRA_CA_CERTS",
|
|
7212
|
+
"SSL_CERT_FILE",
|
|
7213
|
+
"SSL_CERT_DIR",
|
|
7214
|
+
"SystemRoot",
|
|
7215
|
+
"windir",
|
|
7216
|
+
"APPDATA",
|
|
7217
|
+
"LOCALAPPDATA",
|
|
7218
|
+
"TEMP",
|
|
7219
|
+
"TMP",
|
|
7220
|
+
"PATHEXT",
|
|
7221
|
+
"ComSpec",
|
|
7222
|
+
"NUMBER_OF_PROCESSORS"
|
|
7223
|
+
];
|
|
7224
|
+
function resolveChildEnv(opts = {}) {
|
|
7225
|
+
const extra = opts.extra ?? {};
|
|
7226
|
+
if (opts.envMode === "inherit") {
|
|
7227
|
+
const inherited = {};
|
|
7228
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
7229
|
+
if (typeof value === "string") {
|
|
7230
|
+
inherited[key] = value;
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
return { ...inherited, ...extra };
|
|
7234
|
+
}
|
|
7235
|
+
const minimal = {};
|
|
7236
|
+
const keys = /* @__PURE__ */ new Set([...MINIMAL_ENV_KEYS, ...opts.allowlist ?? []]);
|
|
7237
|
+
for (const key of keys) {
|
|
7238
|
+
const value = process.env[key];
|
|
7239
|
+
if (typeof value === "string") {
|
|
7240
|
+
minimal[key] = value;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
return { ...minimal, ...extra };
|
|
7244
|
+
}
|
|
7245
|
+
function killProcessTree(child, signal, detached) {
|
|
7246
|
+
if (process.platform !== "win32" && detached && typeof child.pid === "number") {
|
|
7247
|
+
try {
|
|
7248
|
+
process.kill(-child.pid, signal);
|
|
7249
|
+
return;
|
|
7250
|
+
} catch {
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
try {
|
|
7254
|
+
child.kill(signal);
|
|
7255
|
+
} catch {
|
|
7256
|
+
}
|
|
7257
|
+
}
|
|
7258
|
+
function resolveManagedCommand(key, optionsCommands, routeCommand, presetCommand) {
|
|
7259
|
+
const resolved = optionsCommands?.[key]?.trim() || routeCommand?.trim() || presetCommand?.trim();
|
|
7260
|
+
if (!resolved) {
|
|
7261
|
+
throw new AiConnectError(
|
|
7262
|
+
"validation_error",
|
|
7263
|
+
`No managed command is configured for "${key}".`
|
|
7264
|
+
);
|
|
7265
|
+
}
|
|
7266
|
+
return resolved;
|
|
7267
|
+
}
|
|
7268
|
+
var trackedChildren = /* @__PURE__ */ new Set();
|
|
7269
|
+
var exitHookRegistered = false;
|
|
7270
|
+
function killAllTrackedChildren() {
|
|
7271
|
+
for (const tracked of trackedChildren) {
|
|
7272
|
+
killProcessTree(tracked.child, "SIGKILL", tracked.detached);
|
|
7273
|
+
}
|
|
7274
|
+
trackedChildren.clear();
|
|
7275
|
+
}
|
|
7276
|
+
function ensureExitHook() {
|
|
7277
|
+
if (exitHookRegistered) {
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
exitHookRegistered = true;
|
|
7281
|
+
process.on("exit", () => {
|
|
7282
|
+
killAllTrackedChildren();
|
|
7283
|
+
});
|
|
7284
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
7285
|
+
const handler = () => {
|
|
7286
|
+
killAllTrackedChildren();
|
|
7287
|
+
process.removeListener(signal, handler);
|
|
7288
|
+
if (process.listenerCount(signal) === 0) {
|
|
7289
|
+
process.kill(process.pid, signal);
|
|
7290
|
+
}
|
|
7291
|
+
};
|
|
7292
|
+
process.on(signal, handler);
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
function trackChild(child, detached) {
|
|
7296
|
+
ensureExitHook();
|
|
7297
|
+
const entry = { child, detached };
|
|
7298
|
+
trackedChildren.add(entry);
|
|
7299
|
+
const untrack = () => {
|
|
7300
|
+
trackedChildren.delete(entry);
|
|
7301
|
+
};
|
|
7302
|
+
child.once("close", untrack);
|
|
7303
|
+
child.once("exit", untrack);
|
|
7304
|
+
return untrack;
|
|
7305
|
+
}
|
|
7306
|
+
|
|
7307
|
+
// src/acp.ts
|
|
6766
7308
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
6767
7309
|
var DEFAULT_SESSION_CACHE_IDLE_TTL_MS = 6e4;
|
|
7310
|
+
var MAX_ACP_STDERR_CHARS = 64 * 1024;
|
|
7311
|
+
var MAX_ACP_STREAM_QUEUE = 1024;
|
|
6768
7312
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
6769
7313
|
[".png", "image/png"],
|
|
6770
7314
|
[".jpg", "image/jpeg"],
|
|
@@ -7053,24 +7597,29 @@ function prepareClaudeLaunchRuntime(base, launch) {
|
|
|
7053
7597
|
launch
|
|
7054
7598
|
};
|
|
7055
7599
|
}
|
|
7600
|
+
var ACP_LAUNCH_PREPARERS = {
|
|
7601
|
+
"opencode-acp": prepareOpenCodeLaunchRuntime,
|
|
7602
|
+
"codex-acp": prepareCodexLaunchRuntime,
|
|
7603
|
+
"claude-code-acp": prepareClaudeLaunchRuntime
|
|
7604
|
+
};
|
|
7056
7605
|
async function prepareAcpLaunchRuntime(route, options, commandLine, cwdOverride) {
|
|
7057
7606
|
const launch = normalizeLaunchOptions(route, options);
|
|
7058
7607
|
const base = {
|
|
7059
7608
|
commandLine,
|
|
7060
7609
|
cwd: path.resolve(cwdOverride ?? options?.cwd ?? process.cwd()),
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
}
|
|
7610
|
+
// SYS-2: minimal env by default — do NOT hand the host's full process.env
|
|
7611
|
+
// (other-provider keys, CI tokens) to third-party ACP agents. Extra env is
|
|
7612
|
+
// layered via options.env.
|
|
7613
|
+
env: resolveChildEnv({ extra: options?.env ?? {} })
|
|
7065
7614
|
};
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
if (route.transport.id === "codex-acp") {
|
|
7070
|
-
return prepareCodexLaunchRuntime(base, launch);
|
|
7615
|
+
const preparer = ACP_LAUNCH_PREPARERS[route.transport.id];
|
|
7616
|
+
if (preparer) {
|
|
7617
|
+
return await preparer(base, launch);
|
|
7071
7618
|
}
|
|
7072
|
-
if (
|
|
7073
|
-
|
|
7619
|
+
if (launch.contextMode !== "workspace" || launch.skillsMode !== "default") {
|
|
7620
|
+
console.warn(
|
|
7621
|
+
`[ai-connect] ACP transport "${route.transport.id}" has no launch-runtime preparer; requested contextMode/skillsMode isolation is not applied.`
|
|
7622
|
+
);
|
|
7074
7623
|
}
|
|
7075
7624
|
return {
|
|
7076
7625
|
...base,
|
|
@@ -7102,8 +7651,27 @@ function mimeTypeFromPath(filePath) {
|
|
|
7102
7651
|
function isTextPath(filePath) {
|
|
7103
7652
|
return TEXT_EXTENSIONS2.has(path.extname(filePath).toLowerCase());
|
|
7104
7653
|
}
|
|
7105
|
-
function
|
|
7106
|
-
|
|
7654
|
+
async function resolveRealPathTolerant(target) {
|
|
7655
|
+
try {
|
|
7656
|
+
return await fs.realpath(target);
|
|
7657
|
+
} catch {
|
|
7658
|
+
const parent = path.dirname(target);
|
|
7659
|
+
if (parent === target) {
|
|
7660
|
+
return target;
|
|
7661
|
+
}
|
|
7662
|
+
const realParent = await resolveRealPathTolerant(parent);
|
|
7663
|
+
return path.join(realParent, path.basename(target));
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
async function isWithinRoot(rootDir, targetPath) {
|
|
7667
|
+
let realRoot;
|
|
7668
|
+
try {
|
|
7669
|
+
realRoot = await fs.realpath(rootDir);
|
|
7670
|
+
} catch {
|
|
7671
|
+
realRoot = path.resolve(rootDir);
|
|
7672
|
+
}
|
|
7673
|
+
const realTarget = await resolveRealPathTolerant(path.resolve(targetPath));
|
|
7674
|
+
const relative = path.relative(realRoot, realTarget);
|
|
7107
7675
|
return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
7108
7676
|
}
|
|
7109
7677
|
function buildMessageTranscript(context) {
|
|
@@ -7530,46 +8098,58 @@ function extractTextChunk(update) {
|
|
|
7530
8098
|
}
|
|
7531
8099
|
return typeof content.text === "string" && content.text.length > 0 ? content.text : void 0;
|
|
7532
8100
|
}
|
|
8101
|
+
function isAuthErrorMessage(message) {
|
|
8102
|
+
return /unauthor|authentication|invalid[_\s-]?(api[_\s-]?key|credential|token)|forbidden|\b40[13]\b/i.test(
|
|
8103
|
+
message
|
|
8104
|
+
);
|
|
8105
|
+
}
|
|
7533
8106
|
function normalizeAcpError(error) {
|
|
7534
8107
|
if (error instanceof AiConnectError) {
|
|
7535
8108
|
return error;
|
|
7536
8109
|
}
|
|
8110
|
+
if (error instanceof Error) {
|
|
8111
|
+
const message = error.message || "ACP transport failed.";
|
|
8112
|
+
const errno = error.code;
|
|
8113
|
+
if (errno === "ENOENT" || errno === "EACCES" || errno === "ENOEXEC" || message.toLowerCase().includes("timed out")) {
|
|
8114
|
+
return new AiConnectError("local_harness_unavailable", message);
|
|
8115
|
+
}
|
|
8116
|
+
if (isAuthErrorMessage(message)) {
|
|
8117
|
+
return new AiConnectError("auth_error", message);
|
|
8118
|
+
}
|
|
8119
|
+
return new AiConnectError("temporary_unavailable", message);
|
|
8120
|
+
}
|
|
7537
8121
|
if (isRecord2(error)) {
|
|
7538
8122
|
const code = typeof error.code === "number" ? error.code : void 0;
|
|
7539
8123
|
const message = typeof error.message === "string" ? error.message : "ACP transport failed.";
|
|
7540
8124
|
if (code === -32601 || code === -32602) {
|
|
7541
8125
|
return new AiConnectError("not_supported", message);
|
|
7542
8126
|
}
|
|
7543
|
-
if (message
|
|
8127
|
+
if (isAuthErrorMessage(message)) {
|
|
7544
8128
|
return new AiConnectError("auth_error", message);
|
|
7545
8129
|
}
|
|
7546
8130
|
return new AiConnectError("temporary_unavailable", message);
|
|
7547
8131
|
}
|
|
7548
|
-
if (error instanceof Error) {
|
|
7549
|
-
const message = error.message;
|
|
7550
|
-
if ("code" in error && error.code === "ENOENT") {
|
|
7551
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7552
|
-
}
|
|
7553
|
-
if (message.toLowerCase().includes("timed out")) {
|
|
7554
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7555
|
-
}
|
|
7556
|
-
return new AiConnectError("temporary_unavailable", message);
|
|
7557
|
-
}
|
|
7558
8132
|
return new AiConnectError(
|
|
7559
8133
|
"temporary_unavailable",
|
|
7560
8134
|
"ACP transport failed."
|
|
7561
8135
|
);
|
|
7562
8136
|
}
|
|
7563
8137
|
function resolveAcpCommand(route, options) {
|
|
7564
|
-
const
|
|
7565
|
-
const
|
|
7566
|
-
|
|
8138
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
8139
|
+
const optionsOverride = options?.commands?.[key] ?? options?.commands?.[route.transport.id] ?? options?.commands?.[route.provider];
|
|
8140
|
+
try {
|
|
8141
|
+
return resolveManagedCommand(
|
|
8142
|
+
key,
|
|
8143
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
8144
|
+
route.transport.command,
|
|
8145
|
+
AI_CONNECT_DEFAULT_ACP_COMMANDS[key]
|
|
8146
|
+
);
|
|
8147
|
+
} catch {
|
|
7567
8148
|
throw new AiConnectError(
|
|
7568
8149
|
"validation_error",
|
|
7569
8150
|
`No ACP command is configured for route "${route.id}".`
|
|
7570
8151
|
);
|
|
7571
8152
|
}
|
|
7572
|
-
return resolved;
|
|
7573
8153
|
}
|
|
7574
8154
|
function resolveAcpExecutable(route, options) {
|
|
7575
8155
|
return splitCommandLine(resolveAcpCommand(route, options)).command;
|
|
@@ -7720,14 +8300,7 @@ async function settleClosePromise(closePromise, timeoutMs = 1500) {
|
|
|
7720
8300
|
]);
|
|
7721
8301
|
}
|
|
7722
8302
|
function killAcpProcess(child, signal) {
|
|
7723
|
-
|
|
7724
|
-
try {
|
|
7725
|
-
process.kill(-child.pid, signal);
|
|
7726
|
-
return;
|
|
7727
|
-
} catch {
|
|
7728
|
-
}
|
|
7729
|
-
}
|
|
7730
|
-
child.kill(signal);
|
|
8303
|
+
killProcessTree(child, signal, process.platform !== "win32");
|
|
7731
8304
|
}
|
|
7732
8305
|
var AcpConnection = class {
|
|
7733
8306
|
commandLine;
|
|
@@ -7859,6 +8432,9 @@ var AcpConnection = class {
|
|
|
7859
8432
|
if (stderrSlice.length > 0) {
|
|
7860
8433
|
warnings.push(stderrSlice);
|
|
7861
8434
|
}
|
|
8435
|
+
if (this.stderr.length > MAX_ACP_STDERR_CHARS) {
|
|
8436
|
+
this.stderr = this.stderr.slice(-MAX_ACP_STDERR_CHARS);
|
|
8437
|
+
}
|
|
7862
8438
|
const text = this.activePrompt.text.trim();
|
|
7863
8439
|
if (this.failOnHarnessOnlyTurn && this.activePrompt.harnessMarkers.length > 0 && text.length === 0) {
|
|
7864
8440
|
this.activePrompt = void 0;
|
|
@@ -8028,6 +8604,12 @@ var AcpConnection = class {
|
|
|
8028
8604
|
this.stderr = "";
|
|
8029
8605
|
this.pending.clear();
|
|
8030
8606
|
this.messageQueue = Promise.resolve();
|
|
8607
|
+
child.stdin?.on("error", (error) => {
|
|
8608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8609
|
+
this.stderr += `[ai-connect] ACP stdin error: ${message}
|
|
8610
|
+
`;
|
|
8611
|
+
});
|
|
8612
|
+
trackChild(child, process.platform !== "win32");
|
|
8031
8613
|
child.stdout.setEncoding("utf8");
|
|
8032
8614
|
child.stdout.on("data", (chunk) => {
|
|
8033
8615
|
this.buffer += chunk;
|
|
@@ -8091,8 +8673,14 @@ var AcpConnection = class {
|
|
|
8091
8673
|
if (!stdin) {
|
|
8092
8674
|
return;
|
|
8093
8675
|
}
|
|
8094
|
-
|
|
8676
|
+
try {
|
|
8677
|
+
stdin.write(`${JSON.stringify(message)}
|
|
8095
8678
|
`);
|
|
8679
|
+
} catch (error) {
|
|
8680
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
8681
|
+
this.stderr += `[ai-connect] ACP stdin write error (reply): ${messageText}
|
|
8682
|
+
`;
|
|
8683
|
+
}
|
|
8096
8684
|
}
|
|
8097
8685
|
failPending(error) {
|
|
8098
8686
|
for (const entry of this.pending.values()) {
|
|
@@ -8113,7 +8701,7 @@ var AcpConnection = class {
|
|
|
8113
8701
|
}
|
|
8114
8702
|
if (message.method === "readTextFile") {
|
|
8115
8703
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
8116
|
-
if (!filePath || !isWithinRoot(this.cwd, filePath)) {
|
|
8704
|
+
if (!filePath || !await isWithinRoot(this.cwd, filePath)) {
|
|
8117
8705
|
this.reply({
|
|
8118
8706
|
jsonrpc: "2.0",
|
|
8119
8707
|
id: message.id,
|
|
@@ -8146,7 +8734,7 @@ var AcpConnection = class {
|
|
|
8146
8734
|
}
|
|
8147
8735
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
8148
8736
|
const content = typeof params.content === "string" ? params.content : void 0;
|
|
8149
|
-
if (!filePath || content === void 0 || !isWithinRoot(this.cwd, filePath)) {
|
|
8737
|
+
if (!filePath || content === void 0 || !await isWithinRoot(this.cwd, filePath)) {
|
|
8150
8738
|
this.reply({
|
|
8151
8739
|
jsonrpc: "2.0",
|
|
8152
8740
|
id: message.id,
|
|
@@ -8420,8 +9008,20 @@ var AcpConnection = class {
|
|
|
8420
9008
|
);
|
|
8421
9009
|
return;
|
|
8422
9010
|
}
|
|
8423
|
-
|
|
9011
|
+
try {
|
|
9012
|
+
stdin.write(`${JSON.stringify(payload)}
|
|
8424
9013
|
`);
|
|
9014
|
+
} catch (error) {
|
|
9015
|
+
clearTimeout(timer);
|
|
9016
|
+
this.pending.delete(id);
|
|
9017
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
9018
|
+
reject(
|
|
9019
|
+
new AiConnectError(
|
|
9020
|
+
"local_harness_unavailable",
|
|
9021
|
+
`ACP command "${this.commandLine}" stdin write failed: ${messageText}`
|
|
9022
|
+
)
|
|
9023
|
+
);
|
|
9024
|
+
}
|
|
8425
9025
|
});
|
|
8426
9026
|
}
|
|
8427
9027
|
};
|
|
@@ -8692,6 +9292,10 @@ function createAcpTransportManager(options) {
|
|
|
8692
9292
|
let notify;
|
|
8693
9293
|
const onDelta = (text) => {
|
|
8694
9294
|
deltas.push(text);
|
|
9295
|
+
if (deltas.length > MAX_ACP_STREAM_QUEUE) {
|
|
9296
|
+
const overflow = deltas.splice(0, deltas.length - MAX_ACP_STREAM_QUEUE);
|
|
9297
|
+
deltas.unshift(overflow.join(""));
|
|
9298
|
+
}
|
|
8695
9299
|
notify?.();
|
|
8696
9300
|
};
|
|
8697
9301
|
let settled = false;
|
|
@@ -8829,44 +9433,29 @@ ${message.content}`;
|
|
|
8829
9433
|
}).join("\n\n");
|
|
8830
9434
|
}
|
|
8831
9435
|
function resolveCliCommand(route, options) {
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
}
|
|
8835
|
-
const candidates = [
|
|
8836
|
-
`${route.provider}:${route.transport.id}`,
|
|
8837
|
-
route.transport.id,
|
|
8838
|
-
route.provider
|
|
8839
|
-
];
|
|
8840
|
-
for (const candidate of candidates) {
|
|
8841
|
-
const override = options?.commands?.[candidate]?.trim();
|
|
8842
|
-
if (override) {
|
|
8843
|
-
return override;
|
|
8844
|
-
}
|
|
8845
|
-
}
|
|
9436
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
9437
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
8846
9438
|
const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
9439
|
+
const presetCommand = (presetId ? getCliTransportPreset(presetId).command.trim() : "") || AI_CONNECT_DEFAULT_CLI_COMMANDS[key] || void 0;
|
|
9440
|
+
try {
|
|
9441
|
+
return resolveManagedCommand(
|
|
9442
|
+
key,
|
|
9443
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
9444
|
+
route.transport.command,
|
|
9445
|
+
presetCommand
|
|
9446
|
+
);
|
|
9447
|
+
} catch {
|
|
9448
|
+
throw new AiConnectError(
|
|
9449
|
+
"validation_error",
|
|
9450
|
+
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
9451
|
+
);
|
|
8856
9452
|
}
|
|
8857
|
-
throw new AiConnectError(
|
|
8858
|
-
"validation_error",
|
|
8859
|
-
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
8860
|
-
);
|
|
8861
9453
|
}
|
|
8862
9454
|
function resolveCliExecutable(route, options) {
|
|
8863
9455
|
return splitCommandLine2(resolveCliCommand(route, options)).command;
|
|
8864
9456
|
}
|
|
8865
9457
|
function buildCliEnvironment(options) {
|
|
8866
|
-
return {
|
|
8867
|
-
...process.env,
|
|
8868
|
-
...options?.env ?? {}
|
|
8869
|
-
};
|
|
9458
|
+
return resolveChildEnv({ extra: options?.env ?? {} });
|
|
8870
9459
|
}
|
|
8871
9460
|
function buildCliCwd(context, options) {
|
|
8872
9461
|
return path2.resolve(
|
|
@@ -8967,9 +9556,13 @@ function resolveCliTransportOptions(route) {
|
|
|
8967
9556
|
} : void 0;
|
|
8968
9557
|
const merged = {
|
|
8969
9558
|
...preset?.options ?? {},
|
|
9559
|
+
// `...route.transport.cli` already spreads argsTemplate + parser (when set on
|
|
9560
|
+
// the route) over the preset defaults, so the previously-present conditional
|
|
9561
|
+
// re-spreads of those two fields were dead/redundant (they re-assigned the
|
|
9562
|
+
// identical value) and have been dropped. fileInput/preset below are NOT
|
|
9563
|
+
// redundant: fileInput is a per-field DEEP merge (mergedFileInput) and preset
|
|
9564
|
+
// is derived (route.transport.cli.preset ?? default), not a plain passthrough.
|
|
8970
9565
|
...route.transport.cli ?? {},
|
|
8971
|
-
...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
|
|
8972
|
-
...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
|
|
8973
9566
|
...mergedFileInput ? { fileInput: mergedFileInput } : {},
|
|
8974
9567
|
...presetId ? { preset: presetId } : {}
|
|
8975
9568
|
};
|
|
@@ -9066,7 +9659,7 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
|
|
|
9066
9659
|
}
|
|
9067
9660
|
return values.outputFile;
|
|
9068
9661
|
default:
|
|
9069
|
-
if (templateArg.startsWith("--")) {
|
|
9662
|
+
if (templateArg.startsWith("--") && templateArg.length > 2) {
|
|
9070
9663
|
parameterKeys.push(templateArg);
|
|
9071
9664
|
}
|
|
9072
9665
|
return templateArg;
|
|
@@ -9219,8 +9812,19 @@ function getValueByPath(value, dotPath) {
|
|
|
9219
9812
|
}
|
|
9220
9813
|
return current;
|
|
9221
9814
|
}
|
|
9222
|
-
function
|
|
9223
|
-
|
|
9815
|
+
function parseCliJson(raw, context) {
|
|
9816
|
+
try {
|
|
9817
|
+
return JSON.parse(raw);
|
|
9818
|
+
} catch (error) {
|
|
9819
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
9820
|
+
throw new AiConnectError(
|
|
9821
|
+
"temporary_unavailable",
|
|
9822
|
+
`${context} produced output that is not valid JSON (${detail}).`
|
|
9823
|
+
);
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
function splitJsonlLines(stdout, context = "CLI jsonl parser") {
|
|
9827
|
+
return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => parseCliJson(line, context));
|
|
9224
9828
|
}
|
|
9225
9829
|
function normalizeErrorMessage(value) {
|
|
9226
9830
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -9246,8 +9850,8 @@ function findJsonlSelection(entries, selector) {
|
|
|
9246
9850
|
}
|
|
9247
9851
|
return void 0;
|
|
9248
9852
|
}
|
|
9249
|
-
function parseGenericJsonCli(stdout, parser) {
|
|
9250
|
-
const payload =
|
|
9853
|
+
function parseGenericJsonCli(stdout, parser, context) {
|
|
9854
|
+
const payload = parseCliJson(stdout, context);
|
|
9251
9855
|
const errorValue = parser.errorPath ? getValueByPath(payload, parser.errorPath) : void 0;
|
|
9252
9856
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9253
9857
|
if (errorMessage) {
|
|
@@ -9268,8 +9872,8 @@ function parseGenericJsonCli(stdout, parser) {
|
|
|
9268
9872
|
data: payload
|
|
9269
9873
|
};
|
|
9270
9874
|
}
|
|
9271
|
-
function parseGenericJsonlCli(stdout, parser) {
|
|
9272
|
-
const entries = splitJsonlLines(stdout);
|
|
9875
|
+
function parseGenericJsonlCli(stdout, parser, context) {
|
|
9876
|
+
const entries = splitJsonlLines(stdout, context);
|
|
9273
9877
|
const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
|
|
9274
9878
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9275
9879
|
if (errorMessage) {
|
|
@@ -9343,7 +9947,7 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9343
9947
|
);
|
|
9344
9948
|
}
|
|
9345
9949
|
const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
|
|
9346
|
-
const payload =
|
|
9950
|
+
const payload = parseCliJson(stdout, "CLI discovery json parser");
|
|
9347
9951
|
const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
|
|
9348
9952
|
return Array.isArray(arr) ? arr : [];
|
|
9349
9953
|
})();
|
|
@@ -9356,8 +9960,8 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9356
9960
|
}
|
|
9357
9961
|
return models;
|
|
9358
9962
|
}
|
|
9359
|
-
function parseClaudeCli(stdout) {
|
|
9360
|
-
const payload =
|
|
9963
|
+
function parseClaudeCli(stdout, context) {
|
|
9964
|
+
const payload = parseCliJson(stdout, context);
|
|
9361
9965
|
const text = typeof payload.result === "string" ? payload.result : typeof payload.response === "string" ? payload.response : typeof payload.text === "string" ? payload.text : void 0;
|
|
9362
9966
|
if (!text) {
|
|
9363
9967
|
throw new AiConnectError(
|
|
@@ -9372,8 +9976,8 @@ function parseClaudeCli(stdout) {
|
|
|
9372
9976
|
data: payload
|
|
9373
9977
|
};
|
|
9374
9978
|
}
|
|
9375
|
-
function parseCodexCli(stdout, outputFileContent) {
|
|
9376
|
-
const events = splitJsonlLines(stdout);
|
|
9979
|
+
function parseCodexCli(stdout, outputFileContent, context) {
|
|
9980
|
+
const events = splitJsonlLines(stdout, context);
|
|
9377
9981
|
let text = outputFileContent?.trim() || void 0;
|
|
9378
9982
|
let usage;
|
|
9379
9983
|
for (const event of events) {
|
|
@@ -9423,6 +10027,7 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9423
10027
|
);
|
|
9424
10028
|
}
|
|
9425
10029
|
const cliOptions = resolveCliTransportOptions(route);
|
|
10030
|
+
const parseContext = `CLI transport "${route.transport.id}"${cliOptions.preset ? ` (preset ${cliOptions.preset})` : ""}`;
|
|
9426
10031
|
if (route.transport.cli?.parser) {
|
|
9427
10032
|
const parser = cliOptions.parser;
|
|
9428
10033
|
if (!parser) {
|
|
@@ -9435,9 +10040,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9435
10040
|
case "text":
|
|
9436
10041
|
return parseTextCli(result.stdout, parser);
|
|
9437
10042
|
case "json":
|
|
9438
|
-
return parseGenericJsonCli(stdout, parser);
|
|
10043
|
+
return parseGenericJsonCli(stdout, parser, parseContext);
|
|
9439
10044
|
default:
|
|
9440
|
-
return parseGenericJsonlCli(stdout, parser);
|
|
10045
|
+
return parseGenericJsonlCli(stdout, parser, parseContext);
|
|
9441
10046
|
}
|
|
9442
10047
|
}
|
|
9443
10048
|
switch (cliOptions.preset) {
|
|
@@ -9445,9 +10050,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9445
10050
|
return parseTextCli(stdout, { kind: "text" });
|
|
9446
10051
|
case "claude":
|
|
9447
10052
|
case "openclaude":
|
|
9448
|
-
return parseClaudeCli(stdout);
|
|
10053
|
+
return parseClaudeCli(stdout, parseContext);
|
|
9449
10054
|
case "codex":
|
|
9450
|
-
return parseCodexCli(stdout, outputFileContent);
|
|
10055
|
+
return parseCodexCli(stdout, outputFileContent, parseContext);
|
|
9451
10056
|
default:
|
|
9452
10057
|
throw new AiConnectError(
|
|
9453
10058
|
"not_supported",
|
|
@@ -9461,29 +10066,27 @@ function capturePhase(phases, name, startedAt) {
|
|
|
9461
10066
|
durationMs: Date.now() - startedAt
|
|
9462
10067
|
});
|
|
9463
10068
|
}
|
|
10069
|
+
var MAX_CLI_OUTPUT_CHARS = 64 * 1024 * 1024;
|
|
9464
10070
|
async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
9465
10071
|
const spawnStartedAt = Date.now();
|
|
10072
|
+
const detached = process.platform !== "win32";
|
|
9466
10073
|
const child = spawn2(invocation.command, invocation.args, {
|
|
9467
10074
|
cwd: invocation.cwd,
|
|
9468
10075
|
env: invocation.env,
|
|
9469
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10076
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10077
|
+
detached
|
|
9470
10078
|
});
|
|
9471
10079
|
capturePhase(phases, "spawn", spawnStartedAt);
|
|
9472
10080
|
let stdout = "";
|
|
9473
10081
|
let stderr = "";
|
|
9474
10082
|
child.stdout.setEncoding("utf8");
|
|
9475
|
-
child.stdout.on("data", (chunk) => {
|
|
9476
|
-
stdout += chunk;
|
|
9477
|
-
});
|
|
9478
10083
|
child.stderr.setEncoding("utf8");
|
|
9479
|
-
child.stderr.on("data", (chunk) => {
|
|
9480
|
-
stderr += chunk;
|
|
9481
|
-
});
|
|
9482
10084
|
const executeStartedAt = Date.now();
|
|
9483
10085
|
return await new Promise((resolve, reject) => {
|
|
9484
10086
|
let abortListener;
|
|
10087
|
+
let overflowed = false;
|
|
9485
10088
|
const timer = setTimeout(() => {
|
|
9486
|
-
child
|
|
10089
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9487
10090
|
reject(
|
|
9488
10091
|
new AiConnectError(
|
|
9489
10092
|
"temporary_unavailable",
|
|
@@ -9498,15 +10101,43 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9498
10101
|
abortListener = void 0;
|
|
9499
10102
|
}
|
|
9500
10103
|
};
|
|
10104
|
+
const enforceBufferCap = () => {
|
|
10105
|
+
if (overflowed || stdout.length + stderr.length <= MAX_CLI_OUTPUT_CHARS) {
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
10108
|
+
overflowed = true;
|
|
10109
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
10110
|
+
cleanup();
|
|
10111
|
+
reject(
|
|
10112
|
+
new AiConnectError(
|
|
10113
|
+
"temporary_unavailable",
|
|
10114
|
+
`CLI transport "${quoteArg(invocation.command)}" exceeded the ${MAX_CLI_OUTPUT_CHARS}-character output cap.`
|
|
10115
|
+
)
|
|
10116
|
+
);
|
|
10117
|
+
};
|
|
10118
|
+
child.stdout.on("data", (chunk) => {
|
|
10119
|
+
if (overflowed) {
|
|
10120
|
+
return;
|
|
10121
|
+
}
|
|
10122
|
+
stdout += chunk;
|
|
10123
|
+
enforceBufferCap();
|
|
10124
|
+
});
|
|
10125
|
+
child.stderr.on("data", (chunk) => {
|
|
10126
|
+
if (overflowed) {
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
10129
|
+
stderr += chunk;
|
|
10130
|
+
enforceBufferCap();
|
|
10131
|
+
});
|
|
9501
10132
|
if (signal) {
|
|
9502
10133
|
if (signal.aborted) {
|
|
9503
|
-
child
|
|
10134
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9504
10135
|
cleanup();
|
|
9505
10136
|
reject(new AiConnectError("aborted", "CLI transport aborted before execution."));
|
|
9506
10137
|
return;
|
|
9507
10138
|
}
|
|
9508
10139
|
abortListener = () => {
|
|
9509
|
-
child
|
|
10140
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9510
10141
|
cleanup();
|
|
9511
10142
|
reject(new AiConnectError("aborted", "CLI transport aborted."));
|
|
9512
10143
|
};
|
|
@@ -9522,6 +10153,9 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9522
10153
|
);
|
|
9523
10154
|
});
|
|
9524
10155
|
child.once("close", (exitCode) => {
|
|
10156
|
+
if (overflowed) {
|
|
10157
|
+
return;
|
|
10158
|
+
}
|
|
9525
10159
|
cleanup();
|
|
9526
10160
|
capturePhase(phases, "execute", executeStartedAt);
|
|
9527
10161
|
resolve({
|
|
@@ -9625,7 +10259,10 @@ function createCliTransportManager(options) {
|
|
|
9625
10259
|
)
|
|
9626
10260
|
);
|
|
9627
10261
|
} finally {
|
|
9628
|
-
|
|
10262
|
+
try {
|
|
10263
|
+
await cleanupCliInvocation(invocation);
|
|
10264
|
+
} catch {
|
|
10265
|
+
}
|
|
9629
10266
|
}
|
|
9630
10267
|
},
|
|
9631
10268
|
async runPrompt(context) {
|
|
@@ -9665,7 +10302,10 @@ function createCliTransportManager(options) {
|
|
|
9665
10302
|
}
|
|
9666
10303
|
return result;
|
|
9667
10304
|
} finally {
|
|
9668
|
-
|
|
10305
|
+
try {
|
|
10306
|
+
await cleanupCliInvocation(invocation);
|
|
10307
|
+
} catch {
|
|
10308
|
+
}
|
|
9669
10309
|
}
|
|
9670
10310
|
}
|
|
9671
10311
|
};
|
|
@@ -9692,28 +10332,21 @@ function splitCommandLine3(commandLine) {
|
|
|
9692
10332
|
};
|
|
9693
10333
|
}
|
|
9694
10334
|
function resolveServerCommand(route, options) {
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
const preset = AI_CONNECT_DEFAULT_SERVER_COMMANDS[`${route.provider}:${route.transport.id}`];
|
|
9710
|
-
if (preset) {
|
|
9711
|
-
return preset;
|
|
10335
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
10336
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
10337
|
+
try {
|
|
10338
|
+
return resolveManagedCommand(
|
|
10339
|
+
key,
|
|
10340
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
10341
|
+
route.transport.command,
|
|
10342
|
+
AI_CONNECT_DEFAULT_SERVER_COMMANDS[key]
|
|
10343
|
+
);
|
|
10344
|
+
} catch {
|
|
10345
|
+
throw new AiConnectError(
|
|
10346
|
+
"validation_error",
|
|
10347
|
+
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
10348
|
+
);
|
|
9712
10349
|
}
|
|
9713
|
-
throw new AiConnectError(
|
|
9714
|
-
"validation_error",
|
|
9715
|
-
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
9716
|
-
);
|
|
9717
10350
|
}
|
|
9718
10351
|
function resolveServerExecutable(route, options) {
|
|
9719
10352
|
return splitCommandLine3(resolveServerCommand(route, options)).command;
|
|
@@ -9785,9 +10418,12 @@ async function findAvailablePort(host = "127.0.0.1") {
|
|
|
9785
10418
|
async function waitForServerHealth(fetchLike, origin, timeoutMs) {
|
|
9786
10419
|
const deadline = Date.now() + timeoutMs;
|
|
9787
10420
|
let lastError;
|
|
10421
|
+
const perAttemptTimeoutMs = Math.min(2e3, Math.max(250, timeoutMs));
|
|
9788
10422
|
while (Date.now() < deadline) {
|
|
9789
10423
|
try {
|
|
9790
|
-
const response = await fetchLike(joinUrl(origin, "/global/health")
|
|
10424
|
+
const response = await fetchLike(joinUrl(origin, "/global/health"), {
|
|
10425
|
+
signal: AbortSignal.timeout(perAttemptTimeoutMs)
|
|
10426
|
+
});
|
|
9791
10427
|
if (response.ok) {
|
|
9792
10428
|
return;
|
|
9793
10429
|
}
|
|
@@ -9817,14 +10453,14 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9817
10453
|
}
|
|
9818
10454
|
args.push("--hostname", host, "--port", String(port));
|
|
9819
10455
|
const spawnStartedAt = Date.now();
|
|
10456
|
+
const detached = process.platform !== "win32";
|
|
9820
10457
|
const child = spawn3(resolved.command, args, {
|
|
9821
10458
|
cwd: cwdOverride ?? options?.cwd ?? process.cwd(),
|
|
9822
|
-
env: {
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
},
|
|
9826
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10459
|
+
env: resolveChildEnv({ extra: options?.env ?? {} }),
|
|
10460
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10461
|
+
detached
|
|
9827
10462
|
});
|
|
10463
|
+
const untrack = trackChild(child, detached);
|
|
9828
10464
|
let stderr = "";
|
|
9829
10465
|
child.stderr.setEncoding("utf8");
|
|
9830
10466
|
child.stderr.on("data", (chunk) => {
|
|
@@ -9840,7 +10476,8 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9840
10476
|
);
|
|
9841
10477
|
capturePhase2(phases, "health", healthStartedAt);
|
|
9842
10478
|
} catch (error) {
|
|
9843
|
-
|
|
10479
|
+
untrack();
|
|
10480
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9844
10481
|
throw new AiConnectError(
|
|
9845
10482
|
"local_harness_unavailable",
|
|
9846
10483
|
stderr.trim() || (error instanceof Error ? error.message : "Failed to start local server transport.")
|
|
@@ -9848,15 +10485,17 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9848
10485
|
}
|
|
9849
10486
|
return {
|
|
9850
10487
|
origin,
|
|
10488
|
+
child,
|
|
9851
10489
|
dispose: async () => {
|
|
10490
|
+
untrack();
|
|
9852
10491
|
if (child.killed) {
|
|
9853
10492
|
return;
|
|
9854
10493
|
}
|
|
9855
|
-
child
|
|
10494
|
+
killProcessTree(child, "SIGTERM", detached);
|
|
9856
10495
|
await new Promise((resolve) => {
|
|
9857
10496
|
child.once("close", () => resolve());
|
|
9858
10497
|
setTimeout(() => {
|
|
9859
|
-
child
|
|
10498
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9860
10499
|
resolve();
|
|
9861
10500
|
}, 1e3).unref?.();
|
|
9862
10501
|
});
|
|
@@ -9956,7 +10595,17 @@ function createServerTransportManager(options) {
|
|
|
9956
10595
|
const pending = spawnServerInstance(route, options, phases, cwdOverride);
|
|
9957
10596
|
instances.set(key, pending);
|
|
9958
10597
|
try {
|
|
9959
|
-
|
|
10598
|
+
const instance = await pending;
|
|
10599
|
+
if (instance.child) {
|
|
10600
|
+
const evict = () => {
|
|
10601
|
+
if (instances.get(key) === pending) {
|
|
10602
|
+
instances.delete(key);
|
|
10603
|
+
}
|
|
10604
|
+
};
|
|
10605
|
+
instance.child.once("exit", evict);
|
|
10606
|
+
instance.child.once("error", evict);
|
|
10607
|
+
}
|
|
10608
|
+
return instance;
|
|
9960
10609
|
} catch (error) {
|
|
9961
10610
|
instances.delete(key);
|
|
9962
10611
|
throw error;
|
|
@@ -9986,7 +10635,10 @@ function createServerTransportManager(options) {
|
|
|
9986
10635
|
headers: {
|
|
9987
10636
|
"content-type": "application/json"
|
|
9988
10637
|
},
|
|
9989
|
-
body: JSON.stringify({})
|
|
10638
|
+
body: JSON.stringify({}),
|
|
10639
|
+
// H6: honor caller cancel / operation timeout on session creation too
|
|
10640
|
+
// (was only wired on the message fetch).
|
|
10641
|
+
signal: context.abort.signal
|
|
9990
10642
|
});
|
|
9991
10643
|
capturePhase2(phases, "session/create", sessionStartedAt);
|
|
9992
10644
|
if (!sessionResponse.ok) {
|
|
@@ -10055,7 +10707,10 @@ function createServerTransportManager(options) {
|
|
|
10055
10707
|
};
|
|
10056
10708
|
context.telemetry?.captureTransport(transport);
|
|
10057
10709
|
const startedAt = Date.now();
|
|
10058
|
-
const response = await fetchLike(
|
|
10710
|
+
const response = await fetchLike(
|
|
10711
|
+
joinUrl(instance.origin, "/config/providers"),
|
|
10712
|
+
{ signal: context.abort.signal }
|
|
10713
|
+
);
|
|
10059
10714
|
capturePhase2(phases, "config/providers", startedAt);
|
|
10060
10715
|
if (!response.ok) {
|
|
10061
10716
|
throw new AiConnectError(
|
|
@@ -10099,6 +10754,15 @@ function createServerTransportManager(options) {
|
|
|
10099
10754
|
}
|
|
10100
10755
|
|
|
10101
10756
|
// src/local-handlers.ts
|
|
10757
|
+
function normalizeVerifyIssue(error, fallbackCode, fallbackMessage) {
|
|
10758
|
+
const normalized = error instanceof AiConnectError ? error : new AiConnectError(fallbackCode, fallbackMessage);
|
|
10759
|
+
return [
|
|
10760
|
+
{
|
|
10761
|
+
code: "handler_issue",
|
|
10762
|
+
message: normalized.message
|
|
10763
|
+
}
|
|
10764
|
+
];
|
|
10765
|
+
}
|
|
10102
10766
|
function createLocalRouteHandlers(options = {}) {
|
|
10103
10767
|
const acpTransport = createAcpTransportManager(options.acp);
|
|
10104
10768
|
const cliTransport = createCliTransportManager(options.cli);
|
|
@@ -10129,16 +10793,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
10129
10793
|
}
|
|
10130
10794
|
];
|
|
10131
10795
|
} catch (error) {
|
|
10132
|
-
|
|
10796
|
+
return normalizeVerifyIssue(
|
|
10797
|
+
error,
|
|
10133
10798
|
"validation_error",
|
|
10134
10799
|
"ACP route verification failed."
|
|
10135
10800
|
);
|
|
10136
|
-
return [
|
|
10137
|
-
{
|
|
10138
|
-
code: "handler_issue",
|
|
10139
|
-
message: normalized.message
|
|
10140
|
-
}
|
|
10141
|
-
];
|
|
10142
10801
|
}
|
|
10143
10802
|
}
|
|
10144
10803
|
},
|
|
@@ -10177,16 +10836,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
10177
10836
|
}
|
|
10178
10837
|
];
|
|
10179
10838
|
} catch (error) {
|
|
10180
|
-
|
|
10839
|
+
return normalizeVerifyIssue(
|
|
10840
|
+
error,
|
|
10181
10841
|
"validation_error",
|
|
10182
10842
|
"CLI route verification failed."
|
|
10183
10843
|
);
|
|
10184
|
-
return [
|
|
10185
|
-
{
|
|
10186
|
-
code: "handler_issue",
|
|
10187
|
-
message: normalized.message
|
|
10188
|
-
}
|
|
10189
|
-
];
|
|
10190
10844
|
}
|
|
10191
10845
|
}
|
|
10192
10846
|
},
|
|
@@ -10222,16 +10876,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
10222
10876
|
}
|
|
10223
10877
|
];
|
|
10224
10878
|
} catch (error) {
|
|
10225
|
-
|
|
10879
|
+
return normalizeVerifyIssue(
|
|
10880
|
+
error,
|
|
10226
10881
|
"validation_error",
|
|
10227
10882
|
"Server route verification failed."
|
|
10228
10883
|
);
|
|
10229
|
-
return [
|
|
10230
|
-
{
|
|
10231
|
-
code: "handler_issue",
|
|
10232
|
-
message: normalized.message
|
|
10233
|
-
}
|
|
10234
|
-
];
|
|
10235
10884
|
}
|
|
10236
10885
|
}
|
|
10237
10886
|
}
|