@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/local.js
CHANGED
|
@@ -16,7 +16,10 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
16
16
|
// pi takes file/image attachments as separate `@<path>` argv tokens (REQ-024);
|
|
17
17
|
// {files} expands one `@<staged-path>` token per attachment. pi stages all
|
|
18
18
|
// categories (PDFs as-is, no extraction guarantee).
|
|
19
|
-
|
|
19
|
+
// H10 (flag injection): the `--` end-of-options token precedes {files}/{prompt}
|
|
20
|
+
// — both are BARE positionals here — so an `@<path>` reference or a prompt that
|
|
21
|
+
// starts with `-` is parsed by pi as a positional, never as a flag.
|
|
22
|
+
argsTemplate: ["--print", "--model", "{model}", "--", "{files}", "{prompt}"],
|
|
20
23
|
discovery: {
|
|
21
24
|
via: "none"
|
|
22
25
|
},
|
|
@@ -36,13 +39,22 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
36
39
|
transportId: "claude-cli",
|
|
37
40
|
options: {
|
|
38
41
|
preset: "claude",
|
|
39
|
-
|
|
42
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional so a
|
|
43
|
+
// prompt starting with `-` (e.g. "--dangerously-skip-permissions") is parsed
|
|
44
|
+
// by the claude CLI as a positional, not a flag.
|
|
45
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
40
46
|
discovery: {
|
|
41
47
|
via: "acp",
|
|
42
48
|
acp: {
|
|
43
49
|
transportId: "claude-code-acp"
|
|
44
50
|
}
|
|
45
51
|
},
|
|
52
|
+
// SENTINEL (documentation-only): the `__preset__:*` textPath/errorPath values
|
|
53
|
+
// are NOT resolved as live JSON paths. This `parser` block records which
|
|
54
|
+
// dedicated parser a built-in preset maps to (parseClaudeCli); dispatch for a
|
|
55
|
+
// preset route goes through the `cliOptions.preset` switch in cli.ts
|
|
56
|
+
// (parseCliResult), which only reads a parser config when a ROUTE sets its own
|
|
57
|
+
// `cli.parser`. See the mirrored note in cli.ts::parseCliResult.
|
|
46
58
|
parser: {
|
|
47
59
|
kind: "json",
|
|
48
60
|
textPath: "__preset__:claude.result",
|
|
@@ -58,7 +70,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
58
70
|
transportId: "openclaude-cli",
|
|
59
71
|
options: {
|
|
60
72
|
preset: "openclaude",
|
|
61
|
-
|
|
73
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional (see
|
|
74
|
+
// the claude preset above for rationale).
|
|
75
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
76
|
+
// SENTINEL (documentation-only): see the claude preset note — `__preset__:*`
|
|
77
|
+
// paths are markers, not live JSON paths; dispatch is via the preset switch.
|
|
62
78
|
parser: {
|
|
63
79
|
kind: "json",
|
|
64
80
|
textPath: "__preset__:claude.result",
|
|
@@ -77,6 +93,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
77
93
|
// codex exec takes image attachments via `-i/--image <FILE>` flags
|
|
78
94
|
// (REQ-024); {files} expands a `--image <staged-path>` pair per image.
|
|
79
95
|
// Images only — a non-image attachment cleanly rejects pre-spawn.
|
|
96
|
+
// H10 (flag injection): `--` sits AFTER {files} and before the trailing
|
|
97
|
+
// {prompt} positional. Unlike pi, codex files are passed via the explicit
|
|
98
|
+
// `--image <path>` flag (the path is that flag's VALUE, not a positional), so
|
|
99
|
+
// `--` must NOT precede {files} or it would neutralize `--image`. It guards
|
|
100
|
+
// only the {prompt} positional against being parsed as a flag.
|
|
80
101
|
argsTemplate: [
|
|
81
102
|
"exec",
|
|
82
103
|
"--json",
|
|
@@ -85,6 +106,7 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
85
106
|
"--model",
|
|
86
107
|
"{model}",
|
|
87
108
|
"{files}",
|
|
109
|
+
"--",
|
|
88
110
|
"{prompt}"
|
|
89
111
|
],
|
|
90
112
|
discovery: {
|
|
@@ -98,6 +120,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
98
120
|
perFileArgs: ["--image", "{path}"],
|
|
99
121
|
categories: ["image"]
|
|
100
122
|
},
|
|
123
|
+
// SENTINEL (documentation-only): the `__preset__:*` path values below are
|
|
124
|
+
// markers naming the dedicated parser this preset maps to (parseCodexCli), NOT
|
|
125
|
+
// live JSONL selectors. Dispatch for a preset route runs through the
|
|
126
|
+
// `cliOptions.preset` switch in cli.ts::parseCliResult; this block is only ever
|
|
127
|
+
// read as a live parser when a ROUTE overrides `cli.parser`.
|
|
101
128
|
parser: {
|
|
102
129
|
kind: "jsonl",
|
|
103
130
|
text: {
|
|
@@ -134,6 +161,19 @@ var AiConnectError = class extends Error {
|
|
|
134
161
|
this.code = code;
|
|
135
162
|
this.details = details;
|
|
136
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* `Error.prototype.message` is non-enumerable, so a bare `JSON.stringify(err)` silently drops
|
|
166
|
+
* it (LOW, review-report-seed.md). Provide an explicit projection so serializing an
|
|
167
|
+
* `AiConnectError` (logs, wide events, error-response bodies) always carries the message.
|
|
168
|
+
*/
|
|
169
|
+
toJSON() {
|
|
170
|
+
return {
|
|
171
|
+
name: this.name,
|
|
172
|
+
code: this.code,
|
|
173
|
+
message: this.message,
|
|
174
|
+
details: this.details
|
|
175
|
+
};
|
|
176
|
+
}
|
|
137
177
|
};
|
|
138
178
|
function mapAbortError(reason, message) {
|
|
139
179
|
return reason === "timeout" ? new AiConnectError("timeout", message ?? "Operation timed out.") : new AiConnectError("aborted", message ?? "Operation aborted.");
|
|
@@ -161,6 +201,16 @@ function toAiConnectError(error, fallbackCode = "temporary_unavailable") {
|
|
|
161
201
|
// src/config.ts
|
|
162
202
|
var DEFAULT_OPERATIONS = ["text", "image", "file"];
|
|
163
203
|
var DEFAULT_STRATEGY = "priority";
|
|
204
|
+
var CLI_TRANSPORT_ID_BY_PROVIDER = {
|
|
205
|
+
anthropic: "claude-cli",
|
|
206
|
+
openclaude: "openclaude-cli",
|
|
207
|
+
openai: "codex-cli",
|
|
208
|
+
pi: "pi-cli"
|
|
209
|
+
};
|
|
210
|
+
var ACP_TRANSPORT_ID_BY_PROVIDER = {
|
|
211
|
+
anthropic: "claude-code-acp",
|
|
212
|
+
openai: "codex-acp"
|
|
213
|
+
};
|
|
164
214
|
var DEFAULT_FALLBACK_ACTIONS = {
|
|
165
215
|
rate_limit: [
|
|
166
216
|
"rotate-credential",
|
|
@@ -272,33 +322,64 @@ function assert(condition, message) {
|
|
|
272
322
|
throw new AiConnectError("validation_error", message);
|
|
273
323
|
}
|
|
274
324
|
}
|
|
275
|
-
function asPositiveInteger(value, fallback) {
|
|
325
|
+
function asPositiveInteger(value, fallback, fieldLabel) {
|
|
326
|
+
if (value === void 0) {
|
|
327
|
+
return fallback;
|
|
328
|
+
}
|
|
276
329
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
330
|
+
if (fieldLabel) {
|
|
331
|
+
console.warn(
|
|
332
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
277
335
|
return fallback;
|
|
278
336
|
}
|
|
279
337
|
return Math.floor(value);
|
|
280
338
|
}
|
|
281
|
-
function asNonNegativeInteger(value, fallback) {
|
|
339
|
+
function asNonNegativeInteger(value, fallback, fieldLabel) {
|
|
340
|
+
if (value === void 0) {
|
|
341
|
+
return fallback;
|
|
342
|
+
}
|
|
282
343
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
344
|
+
if (fieldLabel) {
|
|
345
|
+
console.warn(
|
|
346
|
+
`[ai-connect] ${fieldLabel} must be a non-negative finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
283
349
|
return fallback;
|
|
284
350
|
}
|
|
285
351
|
return Math.floor(value);
|
|
286
352
|
}
|
|
287
|
-
function asOptionalContextWindow(value) {
|
|
353
|
+
function asOptionalContextWindow(value, fieldLabel) {
|
|
354
|
+
if (value === void 0) {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
288
357
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
358
|
+
if (fieldLabel) {
|
|
359
|
+
console.warn(
|
|
360
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Ignoring.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
289
363
|
return void 0;
|
|
290
364
|
}
|
|
291
365
|
return Math.floor(value);
|
|
292
366
|
}
|
|
293
|
-
function normalizeModelInput(input, accountContextWindow) {
|
|
367
|
+
function normalizeModelInput(input, accountContextWindow, accountId, providerId) {
|
|
294
368
|
if (typeof input === "string") {
|
|
295
369
|
return {
|
|
296
370
|
id: input.trim(),
|
|
297
371
|
...accountContextWindow !== void 0 ? { contextWindow: accountContextWindow } : {}
|
|
298
372
|
};
|
|
299
373
|
}
|
|
374
|
+
assert(
|
|
375
|
+
typeof input.id === "string" && input.id.trim().length > 0,
|
|
376
|
+
`Account "${accountId}" for provider "${providerId}" declared a model entry with a missing or empty "id".`
|
|
377
|
+
);
|
|
300
378
|
const id = input.id.trim();
|
|
301
|
-
const override = asOptionalContextWindow(
|
|
379
|
+
const override = asOptionalContextWindow(
|
|
380
|
+
input.contextWindow,
|
|
381
|
+
`Model "${id}" (account "${accountId}", provider "${providerId}") contextWindow`
|
|
382
|
+
);
|
|
302
383
|
const contextWindow = override ?? accountContextWindow;
|
|
303
384
|
return {
|
|
304
385
|
id,
|
|
@@ -309,7 +390,8 @@ function asFanoutBound(value) {
|
|
|
309
390
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
310
391
|
return Number.POSITIVE_INFINITY;
|
|
311
392
|
}
|
|
312
|
-
|
|
393
|
+
const floored = Math.floor(value);
|
|
394
|
+
return floored <= 0 ? Number.POSITIVE_INFINITY : floored;
|
|
313
395
|
}
|
|
314
396
|
function normalizeFanoutPolicy(policy) {
|
|
315
397
|
return {
|
|
@@ -535,7 +617,7 @@ function normalizeTransport(providerId, input) {
|
|
|
535
617
|
};
|
|
536
618
|
}
|
|
537
619
|
if (descriptor.kind === "cli") {
|
|
538
|
-
const inferredId2 = providerId
|
|
620
|
+
const inferredId2 = CLI_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "cli";
|
|
539
621
|
return {
|
|
540
622
|
kind: "cli",
|
|
541
623
|
id: descriptor.id?.trim() || inferredId2,
|
|
@@ -552,7 +634,7 @@ function normalizeTransport(providerId, input) {
|
|
|
552
634
|
...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
|
|
553
635
|
};
|
|
554
636
|
}
|
|
555
|
-
const inferredId = providerId
|
|
637
|
+
const inferredId = ACP_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "acp";
|
|
556
638
|
const normalizedLaunch = descriptor.launch ? (() => {
|
|
557
639
|
const contextMode = descriptor.launch?.contextMode ?? "workspace";
|
|
558
640
|
const skillsMode = descriptor.launch?.skillsMode ?? "default";
|
|
@@ -649,20 +731,26 @@ function defaultCapabilities(transport, runtime) {
|
|
|
649
731
|
}
|
|
650
732
|
function normalizeCredential(providerId, accountId, transport, input, index) {
|
|
651
733
|
const id = input.id?.trim() || `${accountId}-credential-${index + 1}`;
|
|
652
|
-
const
|
|
734
|
+
const trimmedApiKey = input.apiKey?.trim();
|
|
735
|
+
const trimmedApiKeyEnv = input.apiKeyEnv?.trim();
|
|
736
|
+
const source = input.source?.trim() || (trimmedApiKey ? "literal" : trimmedApiKeyEnv ? "env" : transport.kind === "api" ? "env" : "session");
|
|
653
737
|
if (transport.kind === "api") {
|
|
654
738
|
assert(
|
|
655
|
-
Boolean(
|
|
739
|
+
Boolean(trimmedApiKey) || Boolean(trimmedApiKeyEnv),
|
|
656
740
|
`API account "${accountId}" for provider "${providerId}" requires apiKey or apiKeyEnv credentials.`
|
|
657
741
|
);
|
|
658
742
|
}
|
|
659
743
|
return {
|
|
660
744
|
id,
|
|
661
745
|
source,
|
|
662
|
-
weight: asPositiveInteger(
|
|
746
|
+
weight: asPositiveInteger(
|
|
747
|
+
input.weight,
|
|
748
|
+
1,
|
|
749
|
+
`Credential "${id}" (account "${accountId}", provider "${providerId}") weight`
|
|
750
|
+
),
|
|
663
751
|
metadata: input.metadata ?? {},
|
|
664
|
-
...
|
|
665
|
-
...
|
|
752
|
+
...trimmedApiKey ? { apiKey: trimmedApiKey } : {},
|
|
753
|
+
...trimmedApiKeyEnv ? { apiKeyEnv: trimmedApiKeyEnv } : {},
|
|
666
754
|
...input.apiKeyDelimiter ? { apiKeyDelimiter: input.apiKeyDelimiter } : {}
|
|
667
755
|
};
|
|
668
756
|
}
|
|
@@ -696,6 +784,31 @@ function buildRouteId(route) {
|
|
|
696
784
|
slugify(route.model)
|
|
697
785
|
].join(":");
|
|
698
786
|
}
|
|
787
|
+
function assertRouteIdUniqueness(routes) {
|
|
788
|
+
const byId = /* @__PURE__ */ new Map();
|
|
789
|
+
for (const route of routes) {
|
|
790
|
+
const group = byId.get(route.id);
|
|
791
|
+
if (group) {
|
|
792
|
+
group.push(route);
|
|
793
|
+
} else {
|
|
794
|
+
byId.set(route.id, [route]);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (const [id, group] of byId) {
|
|
798
|
+
if (group.length <= 1) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const distinctModels = [...new Set(group.map((route) => route.model))];
|
|
802
|
+
if (distinctModels.length <= 1) {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const first = group[0];
|
|
806
|
+
assert(
|
|
807
|
+
false,
|
|
808
|
+
`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.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
699
812
|
function routeAliases(route) {
|
|
700
813
|
return [
|
|
701
814
|
route.provider,
|
|
@@ -820,7 +933,8 @@ function defineConfig(input) {
|
|
|
820
933
|
const transport = normalizeTransport(providerId, accountInput.transport);
|
|
821
934
|
const runtime = normalizeRuntime(transport, accountInput.runtime);
|
|
822
935
|
const accountContextWindow = asOptionalContextWindow(
|
|
823
|
-
accountInput.contextWindow
|
|
936
|
+
accountInput.contextWindow,
|
|
937
|
+
`Account "${accountId}" (provider "${providerId}") contextWindow`
|
|
824
938
|
);
|
|
825
939
|
const accountModelAllowlistMode = accountInput.modelAllowlistMode ?? "strict";
|
|
826
940
|
const accountContextMode = accountInput.contextMode ?? "workspace";
|
|
@@ -830,7 +944,9 @@ function defineConfig(input) {
|
|
|
830
944
|
Array.isArray(accountInput.models) && accountInput.models.length > 0,
|
|
831
945
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
832
946
|
);
|
|
833
|
-
const models = accountInput.models.map(
|
|
947
|
+
const models = accountInput.models.map(
|
|
948
|
+
(model) => normalizeModelInput(model, accountContextWindow, accountId, providerId)
|
|
949
|
+
).filter((model) => model.id.length > 0);
|
|
834
950
|
assert(
|
|
835
951
|
models.length > 0,
|
|
836
952
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
@@ -860,8 +976,16 @@ function defineConfig(input) {
|
|
|
860
976
|
credentials,
|
|
861
977
|
capabilities,
|
|
862
978
|
verify,
|
|
863
|
-
weight: asPositiveInteger(
|
|
864
|
-
|
|
979
|
+
weight: asPositiveInteger(
|
|
980
|
+
accountInput.weight,
|
|
981
|
+
1,
|
|
982
|
+
`Account "${accountId}" (provider "${providerId}") weight`
|
|
983
|
+
),
|
|
984
|
+
priority: asNonNegativeInteger(
|
|
985
|
+
accountInput.priority,
|
|
986
|
+
accountIndex,
|
|
987
|
+
`Account "${accountId}" (provider "${providerId}") priority`
|
|
988
|
+
),
|
|
865
989
|
enabled: accountInput.enabled ?? true
|
|
866
990
|
};
|
|
867
991
|
accounts.push(normalizedAccount);
|
|
@@ -908,6 +1032,7 @@ function defineConfig(input) {
|
|
|
908
1032
|
};
|
|
909
1033
|
}
|
|
910
1034
|
assert(routes.length > 0, "At least one enabled route must be produced.");
|
|
1035
|
+
assertRouteIdUniqueness(routes);
|
|
911
1036
|
finalizeRotationCapabilities(routes);
|
|
912
1037
|
const strategy = input.routing?.strategy ?? DEFAULT_STRATEGY;
|
|
913
1038
|
const resolutionInput = input.routing?.resolution;
|
|
@@ -981,6 +1106,10 @@ function resolveRouteSelectors(selectors, routes, policy) {
|
|
|
981
1106
|
}
|
|
982
1107
|
|
|
983
1108
|
// src/fanout.ts
|
|
1109
|
+
function abortedError() {
|
|
1110
|
+
return new AiConnectError("aborted", "Operation aborted.");
|
|
1111
|
+
}
|
|
1112
|
+
var MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
984
1113
|
function createFanoutLimiter(policy, runtime) {
|
|
985
1114
|
const now = () => runtime.now?.() ?? Date.now();
|
|
986
1115
|
const { maxConcurrency, requestsPerSecond, maxCalls } = policy;
|
|
@@ -1044,10 +1173,10 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1044
1173
|
if (index >= 0) {
|
|
1045
1174
|
concurrencyQueue.splice(index, 1);
|
|
1046
1175
|
}
|
|
1047
|
-
rejectWaiter(waiter,
|
|
1176
|
+
rejectWaiter(waiter, abortedError());
|
|
1048
1177
|
};
|
|
1049
1178
|
if (signal.aborted) {
|
|
1050
|
-
reject(
|
|
1179
|
+
reject(abortedError());
|
|
1051
1180
|
return;
|
|
1052
1181
|
}
|
|
1053
1182
|
waiter.signal = signal;
|
|
@@ -1063,7 +1192,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1063
1192
|
}
|
|
1064
1193
|
while (true) {
|
|
1065
1194
|
if (signal?.aborted) {
|
|
1066
|
-
throw
|
|
1195
|
+
throw abortedError();
|
|
1067
1196
|
}
|
|
1068
1197
|
refill();
|
|
1069
1198
|
if (tokens >= 1) {
|
|
@@ -1075,6 +1204,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1075
1204
|
}
|
|
1076
1205
|
}
|
|
1077
1206
|
function delay(ms, signal) {
|
|
1207
|
+
const safeMs = Number.isFinite(ms) && ms >= 0 ? Math.min(ms, MAX_DELAY_MS) : MAX_DELAY_MS;
|
|
1078
1208
|
return new Promise((resolve, reject) => {
|
|
1079
1209
|
let cancelTimer;
|
|
1080
1210
|
const onAbort = () => {
|
|
@@ -1082,11 +1212,11 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1082
1212
|
if (signal) {
|
|
1083
1213
|
signal.removeEventListener("abort", onAbort);
|
|
1084
1214
|
}
|
|
1085
|
-
reject(
|
|
1215
|
+
reject(abortedError());
|
|
1086
1216
|
};
|
|
1087
1217
|
if (signal) {
|
|
1088
1218
|
if (signal.aborted) {
|
|
1089
|
-
reject(
|
|
1219
|
+
reject(abortedError());
|
|
1090
1220
|
return;
|
|
1091
1221
|
}
|
|
1092
1222
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1098,9 +1228,9 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1098
1228
|
resolve();
|
|
1099
1229
|
};
|
|
1100
1230
|
if (runtime.setTimer) {
|
|
1101
|
-
cancelTimer = runtime.setTimer(
|
|
1231
|
+
cancelTimer = runtime.setTimer(safeMs, done);
|
|
1102
1232
|
} else {
|
|
1103
|
-
const handle = setTimeout(done,
|
|
1233
|
+
const handle = setTimeout(done, safeMs);
|
|
1104
1234
|
cancelTimer = () => clearTimeout(handle);
|
|
1105
1235
|
}
|
|
1106
1236
|
});
|
|
@@ -1114,8 +1244,13 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1114
1244
|
);
|
|
1115
1245
|
}
|
|
1116
1246
|
callsStarted += 1;
|
|
1117
|
-
|
|
1118
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
await awaitRateToken(signal);
|
|
1249
|
+
await awaitConcurrencySlot(signal);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
callsStarted -= 1;
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1119
1254
|
let released = false;
|
|
1120
1255
|
return () => {
|
|
1121
1256
|
if (released) {
|
|
@@ -1203,19 +1338,54 @@ function isRemoteUrl(value) {
|
|
|
1203
1338
|
return false;
|
|
1204
1339
|
}
|
|
1205
1340
|
}
|
|
1341
|
+
function mimeTypeAndExtensionFromMediaType(mediaTypeAndParams, fallbackMimeType, fallbackExtension) {
|
|
1342
|
+
const mimeType = mediaTypeAndParams.split(";")[0]?.trim() || fallbackMimeType;
|
|
1343
|
+
const extension2 = mimeType.split("/")[1] ?? fallbackExtension;
|
|
1344
|
+
return { mimeType, extension: extension2 };
|
|
1345
|
+
}
|
|
1206
1346
|
function parseDataUrl(value) {
|
|
1207
|
-
const
|
|
1208
|
-
if (
|
|
1209
|
-
|
|
1347
|
+
const base64Match = /^data:([^,]*?);base64,(.*)$/i.exec(value);
|
|
1348
|
+
if (base64Match) {
|
|
1349
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1350
|
+
base64Match[1] ?? "",
|
|
1351
|
+
"application/octet-stream",
|
|
1352
|
+
"bin"
|
|
1353
|
+
);
|
|
1354
|
+
return {
|
|
1355
|
+
mimeType,
|
|
1356
|
+
extension: extension2,
|
|
1357
|
+
data: base64Match[2] ?? ""
|
|
1358
|
+
};
|
|
1210
1359
|
}
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1360
|
+
const plainMatch = /^data:([^,]*?),(.*)$/i.exec(value);
|
|
1361
|
+
if (plainMatch) {
|
|
1362
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1363
|
+
plainMatch[1] ?? "",
|
|
1364
|
+
"text/plain",
|
|
1365
|
+
"txt"
|
|
1366
|
+
);
|
|
1367
|
+
let decoded;
|
|
1368
|
+
try {
|
|
1369
|
+
decoded = decodeURIComponent(plainMatch[2] ?? "");
|
|
1370
|
+
} catch {
|
|
1371
|
+
throw new AiConnectError(
|
|
1372
|
+
"validation_error",
|
|
1373
|
+
"unsupported data URL encoding"
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
mimeType,
|
|
1378
|
+
extension: extension2,
|
|
1379
|
+
data: encodeBase64(new TextEncoder().encode(decoded))
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (/^data:/i.test(value)) {
|
|
1383
|
+
throw new AiConnectError(
|
|
1384
|
+
"validation_error",
|
|
1385
|
+
"unsupported data URL encoding"
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
return null;
|
|
1219
1389
|
}
|
|
1220
1390
|
function mimeTypeFromName(name) {
|
|
1221
1391
|
const ext = extension(name);
|
|
@@ -1429,9 +1599,61 @@ async function materializePortableFile(file) {
|
|
|
1429
1599
|
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1430
1600
|
}
|
|
1431
1601
|
}
|
|
1602
|
+
if (category === "other") {
|
|
1603
|
+
const base64 = await portableFileToBase64(file);
|
|
1604
|
+
if (base64 !== void 0) {
|
|
1605
|
+
payload.base64 = base64;
|
|
1606
|
+
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1607
|
+
} else if (!uri && !providerFileId) {
|
|
1608
|
+
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.`;
|
|
1609
|
+
payload.droppedReason = droppedReason;
|
|
1610
|
+
payload.warnings = [...payload.warnings ?? [], droppedReason];
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1432
1613
|
return payload;
|
|
1433
1614
|
}
|
|
1434
|
-
function
|
|
1615
|
+
function collapsePathSegments(value) {
|
|
1616
|
+
const normalized = value.replaceAll("\\", "/");
|
|
1617
|
+
const isAbsolute = normalized.startsWith("/");
|
|
1618
|
+
const segments = normalized.split("/");
|
|
1619
|
+
const resolved = [];
|
|
1620
|
+
for (const segment of segments) {
|
|
1621
|
+
if (segment === "" || segment === ".") {
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (segment === "..") {
|
|
1625
|
+
if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
|
|
1626
|
+
resolved.pop();
|
|
1627
|
+
} else if (!isAbsolute) {
|
|
1628
|
+
resolved.push("..");
|
|
1629
|
+
}
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
resolved.push(segment);
|
|
1633
|
+
}
|
|
1634
|
+
return (isAbsolute ? "/" : "") + resolved.join("/");
|
|
1635
|
+
}
|
|
1636
|
+
function applyPathGuard(rawPath, guard) {
|
|
1637
|
+
if (!guard) {
|
|
1638
|
+
return rawPath;
|
|
1639
|
+
}
|
|
1640
|
+
if (guard === "basename") {
|
|
1641
|
+
return basename(rawPath);
|
|
1642
|
+
}
|
|
1643
|
+
const resolved = collapsePathSegments(rawPath);
|
|
1644
|
+
const withinAllowedRoot = guard.allowedRoots.some((root) => {
|
|
1645
|
+
const resolvedRoot = collapsePathSegments(root);
|
|
1646
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}/`);
|
|
1647
|
+
});
|
|
1648
|
+
if (!withinAllowedRoot) {
|
|
1649
|
+
throw new AiConnectError(
|
|
1650
|
+
"validation_error",
|
|
1651
|
+
`Path "${rawPath}" is outside the configured allowedRoots for portable file attachments.`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
return rawPath;
|
|
1655
|
+
}
|
|
1656
|
+
function preparePortableFile(input, options) {
|
|
1435
1657
|
if (isPortableFile(input)) {
|
|
1436
1658
|
return input;
|
|
1437
1659
|
}
|
|
@@ -1457,11 +1679,12 @@ function preparePortableFile(input) {
|
|
|
1457
1679
|
}
|
|
1458
1680
|
};
|
|
1459
1681
|
}
|
|
1682
|
+
const guardedPath = applyPathGuard(input, options?.pathGuard);
|
|
1460
1683
|
return {
|
|
1461
|
-
id: `path:${
|
|
1684
|
+
id: `path:${guardedPath}`,
|
|
1462
1685
|
kind: "path",
|
|
1463
|
-
name: basename(
|
|
1464
|
-
source:
|
|
1686
|
+
name: basename(guardedPath),
|
|
1687
|
+
source: guardedPath
|
|
1465
1688
|
};
|
|
1466
1689
|
}
|
|
1467
1690
|
if (isRemoteReference(input)) {
|
|
@@ -1502,7 +1725,20 @@ function preparePortableFile(input) {
|
|
|
1502
1725
|
}
|
|
1503
1726
|
|
|
1504
1727
|
// src/logging.ts
|
|
1505
|
-
|
|
1728
|
+
function redactBaseUrl(url) {
|
|
1729
|
+
try {
|
|
1730
|
+
const parsed = new URL(url);
|
|
1731
|
+
parsed.username = "";
|
|
1732
|
+
parsed.password = "";
|
|
1733
|
+
parsed.search = "";
|
|
1734
|
+
parsed.hash = "";
|
|
1735
|
+
return parsed.toString();
|
|
1736
|
+
} catch {
|
|
1737
|
+
const withoutHash = url.split("#")[0] ?? url;
|
|
1738
|
+
return withoutHash.split("?")[0] ?? withoutHash;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async function shouldEmitWideEvent(event, options = {}, random = Math.random) {
|
|
1506
1742
|
const sampleRate = options.sampleRate ?? 1;
|
|
1507
1743
|
if ((options.keepErrors ?? true) && event.outcome === "error") {
|
|
1508
1744
|
return true;
|
|
@@ -1536,7 +1772,7 @@ async function shouldEmitWideEvent(event, options = {}) {
|
|
|
1536
1772
|
if (sampleRate >= 1) {
|
|
1537
1773
|
return true;
|
|
1538
1774
|
}
|
|
1539
|
-
return
|
|
1775
|
+
return random() < sampleRate;
|
|
1540
1776
|
}
|
|
1541
1777
|
|
|
1542
1778
|
// src/router.ts
|
|
@@ -1601,6 +1837,19 @@ function buildWeightedOrder(routes) {
|
|
|
1601
1837
|
}
|
|
1602
1838
|
return expanded;
|
|
1603
1839
|
}
|
|
1840
|
+
var ROUTING_STRATEGIES = {
|
|
1841
|
+
priority: (ordered) => ordered,
|
|
1842
|
+
failover: (ordered) => ordered,
|
|
1843
|
+
"round-robin": (ordered, cursor, cursorKey) => {
|
|
1844
|
+
const index = cursor(cursorKey, ordered.length);
|
|
1845
|
+
return rotate(ordered, index);
|
|
1846
|
+
},
|
|
1847
|
+
"weighted-round-robin": (ordered, cursor, cursorKey) => {
|
|
1848
|
+
const weighted = buildWeightedOrder(ordered);
|
|
1849
|
+
const index = cursor(cursorKey, weighted.length);
|
|
1850
|
+
return unique(rotate(weighted, index));
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1604
1853
|
var RouteRegistry = class {
|
|
1605
1854
|
config;
|
|
1606
1855
|
runtime;
|
|
@@ -1671,14 +1920,21 @@ var RouteRegistry = class {
|
|
|
1671
1920
|
const routeIds = request.routeHints?.pool?.length ? resolveRouteSelectors(request.routeHints.pool, this.config.routes) : operationRouting.pool;
|
|
1672
1921
|
const excludedIds = new Set(request.routeHints?.excludeRouteIds ?? []);
|
|
1673
1922
|
const requestedModel = request.routeHints?.model;
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1923
|
+
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));
|
|
1924
|
+
const strategy = request.routeHints?.strategy ?? operationRouting.strategy;
|
|
1925
|
+
const cursorKey = `${operation}:${strategy}:${eligible.map((route) => route.id).join("|")}`;
|
|
1926
|
+
const healthyIds = new Set(
|
|
1927
|
+
eligible.filter((route) => {
|
|
1928
|
+
const state = this.getHealth(route).state;
|
|
1929
|
+
return state !== "cooling_down" && state !== "unavailable";
|
|
1930
|
+
}).map((route) => route.id)
|
|
1931
|
+
);
|
|
1678
1932
|
return this.#order(
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1933
|
+
strategy,
|
|
1934
|
+
eligible,
|
|
1935
|
+
healthyIds,
|
|
1936
|
+
request.routeHints?.preferredProviders,
|
|
1937
|
+
cursorKey
|
|
1682
1938
|
);
|
|
1683
1939
|
}
|
|
1684
1940
|
recordSuccess(route) {
|
|
@@ -1711,20 +1967,12 @@ var RouteRegistry = class {
|
|
|
1711
1967
|
failureCount
|
|
1712
1968
|
});
|
|
1713
1969
|
}
|
|
1714
|
-
#order(strategy,
|
|
1715
|
-
const sorted = baseSort(
|
|
1716
|
-
const
|
|
1717
|
-
const ordered =
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
if (strategy === "round-robin") {
|
|
1722
|
-
const index2 = this.#cursor(cursorKey, ordered.length);
|
|
1723
|
-
return rotate(ordered, index2);
|
|
1724
|
-
}
|
|
1725
|
-
const weighted = buildWeightedOrder(ordered);
|
|
1726
|
-
const index = this.#cursor(cursorKey, weighted.length);
|
|
1727
|
-
return unique(rotate(weighted, index));
|
|
1970
|
+
#order(strategy, eligible, healthyIds, preferredProviders, cursorKey) {
|
|
1971
|
+
const sorted = baseSort(eligible, preferredProviders);
|
|
1972
|
+
const orderedFull = this.config.routing.shuffleOnInit ? this.#shuffle(cursorKey, sorted) : sorted;
|
|
1973
|
+
const ordered = orderedFull.filter((route) => healthyIds.has(route.id));
|
|
1974
|
+
const strategyFn = ROUTING_STRATEGIES[strategy];
|
|
1975
|
+
return strategyFn(ordered, (key, length) => this.#cursor(key, length), cursorKey);
|
|
1728
1976
|
}
|
|
1729
1977
|
#shuffle(key, routes) {
|
|
1730
1978
|
const cached = this.#shuffles.get(key);
|
|
@@ -1828,8 +2076,22 @@ var MODEL_REFERENCE = {
|
|
|
1828
2076
|
"gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
|
|
1829
2077
|
"gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
|
|
1830
2078
|
};
|
|
2079
|
+
var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2080
|
+
"minimal",
|
|
2081
|
+
"low",
|
|
2082
|
+
"medium",
|
|
2083
|
+
"high",
|
|
2084
|
+
"xhigh"
|
|
2085
|
+
]);
|
|
1831
2086
|
function normalizeModelKey(model) {
|
|
1832
2087
|
let key = model.trim().toLowerCase();
|
|
2088
|
+
const lastSlashIndex = key.lastIndexOf("/");
|
|
2089
|
+
if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
|
|
2090
|
+
const tail = key.slice(lastSlashIndex + 1);
|
|
2091
|
+
if (REASONING_EFFORT_SUFFIXES.has(tail)) {
|
|
2092
|
+
key = key.slice(0, lastSlashIndex);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1833
2095
|
const slashIndex = key.indexOf("/");
|
|
1834
2096
|
if (slashIndex > 0) {
|
|
1835
2097
|
key = key.slice(slashIndex + 1);
|
|
@@ -1837,6 +2099,7 @@ function normalizeModelKey(model) {
|
|
|
1837
2099
|
key = key.replace(/:(?:free|beta)$/i, "");
|
|
1838
2100
|
return key.trim();
|
|
1839
2101
|
}
|
|
2102
|
+
var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
|
|
1840
2103
|
function lookupModelRef(model) {
|
|
1841
2104
|
const normalized = normalizeModelKey(model);
|
|
1842
2105
|
if (!normalized) {
|
|
@@ -1859,7 +2122,7 @@ function lookupModelRef(model) {
|
|
|
1859
2122
|
}
|
|
1860
2123
|
let substringWinner;
|
|
1861
2124
|
for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
|
|
1862
|
-
if (normalized.includes(key)) {
|
|
2125
|
+
if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
|
|
1863
2126
|
if (!substringWinner || key.length > substringWinner.key.length) {
|
|
1864
2127
|
substringWinner = entry;
|
|
1865
2128
|
}
|
|
@@ -2091,6 +2354,14 @@ var DEFAULT_LOGGING_SERVICE = "ai-connect";
|
|
|
2091
2354
|
function splitCredentialValues(value, delimiter2) {
|
|
2092
2355
|
return value.split(delimiter2).map((item) => item.trim()).filter(Boolean);
|
|
2093
2356
|
}
|
|
2357
|
+
var ClientToolExecutionError = class extends Error {
|
|
2358
|
+
aiError;
|
|
2359
|
+
constructor(aiError) {
|
|
2360
|
+
super(aiError.message);
|
|
2361
|
+
this.name = "ClientToolExecutionError";
|
|
2362
|
+
this.aiError = aiError;
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2094
2365
|
function materializeRuntimeConfig(config, runtime) {
|
|
2095
2366
|
const routeExpansion = /* @__PURE__ */ new Map();
|
|
2096
2367
|
const routes = [];
|
|
@@ -2105,13 +2376,14 @@ function materializeRuntimeConfig(config, runtime) {
|
|
|
2105
2376
|
continue;
|
|
2106
2377
|
}
|
|
2107
2378
|
const expandedIds = [];
|
|
2379
|
+
const expandedWeight = Math.max(1, Math.floor(route.weight / values.length));
|
|
2108
2380
|
values.forEach((apiKey, index) => {
|
|
2109
2381
|
const credentialId = `${route.credentialId}#${index + 1}`;
|
|
2110
2382
|
const expandedRoute = {
|
|
2111
2383
|
...route,
|
|
2112
2384
|
id: `${route.id}#${index + 1}`,
|
|
2113
2385
|
credentialId,
|
|
2114
|
-
weight:
|
|
2386
|
+
weight: expandedWeight,
|
|
2115
2387
|
credential: {
|
|
2116
2388
|
...route.credential,
|
|
2117
2389
|
id: credentialId,
|
|
@@ -2152,6 +2424,27 @@ async function sleep(delayMs) {
|
|
|
2152
2424
|
}
|
|
2153
2425
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2154
2426
|
}
|
|
2427
|
+
async function runWithAbortRace(handlerPromise, abort) {
|
|
2428
|
+
if (abort.signal.aborted) {
|
|
2429
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2430
|
+
});
|
|
2431
|
+
throw mapAbortError(abort.reason);
|
|
2432
|
+
}
|
|
2433
|
+
let onAbort;
|
|
2434
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2435
|
+
onAbort = () => reject(mapAbortError(abort.reason));
|
|
2436
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2437
|
+
});
|
|
2438
|
+
try {
|
|
2439
|
+
return await Promise.race([handlerPromise, abortPromise]);
|
|
2440
|
+
} finally {
|
|
2441
|
+
if (onAbort) {
|
|
2442
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
2443
|
+
}
|
|
2444
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2155
2448
|
function normalizeWorkingDirectory(value) {
|
|
2156
2449
|
if (value === void 0) {
|
|
2157
2450
|
return void 0;
|
|
@@ -2193,6 +2486,12 @@ function normalizeClientToolRegistry(tools) {
|
|
|
2193
2486
|
const registry = /* @__PURE__ */ new Map();
|
|
2194
2487
|
for (const tool of tools ?? []) {
|
|
2195
2488
|
const normalized = normalizeClientToolDefinition(tool);
|
|
2489
|
+
if (registry.has(normalized.function.name)) {
|
|
2490
|
+
throw new AiConnectError(
|
|
2491
|
+
"validation_error",
|
|
2492
|
+
`Duplicate client tool name "${normalized.function.name}" registered on this client.`
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2196
2495
|
registry.set(normalized.function.name, normalized);
|
|
2197
2496
|
}
|
|
2198
2497
|
return registry;
|
|
@@ -2218,10 +2517,22 @@ function resolveClientToolSelections(selections, registry) {
|
|
|
2218
2517
|
`No client tool named "${name}" is registered on this client.`
|
|
2219
2518
|
);
|
|
2220
2519
|
}
|
|
2520
|
+
if (resolved.has(name)) {
|
|
2521
|
+
throw new AiConnectError(
|
|
2522
|
+
"validation_error",
|
|
2523
|
+
`Duplicate client tool "${name}" in the clientTools selection.`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2221
2526
|
resolved.set(name, registered);
|
|
2222
2527
|
continue;
|
|
2223
2528
|
}
|
|
2224
2529
|
const normalized = normalizeClientToolDefinition(selection);
|
|
2530
|
+
if (resolved.has(normalized.function.name)) {
|
|
2531
|
+
throw new AiConnectError(
|
|
2532
|
+
"validation_error",
|
|
2533
|
+
`Duplicate client tool "${normalized.function.name}" in the clientTools selection.`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2225
2536
|
resolved.set(normalized.function.name, normalized);
|
|
2226
2537
|
}
|
|
2227
2538
|
return Array.from(resolved.values());
|
|
@@ -2334,6 +2645,34 @@ function requiresImageInput(request) {
|
|
|
2334
2645
|
function requiresFileUpload(request) {
|
|
2335
2646
|
return request.attachments.some((file) => isPortableDocumentFile(file));
|
|
2336
2647
|
}
|
|
2648
|
+
function resolveRequiredCapabilities(request) {
|
|
2649
|
+
const requiresSchema = requiresToolSchema(request);
|
|
2650
|
+
const requiresClientTools = requiresClientToolExecution(request);
|
|
2651
|
+
const requiresImage = requiresImageInput(request);
|
|
2652
|
+
const requiresFile = requiresFileUpload(request);
|
|
2653
|
+
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2654
|
+
const projected = requiresAnyCapability ? {
|
|
2655
|
+
...request,
|
|
2656
|
+
routeHints: {
|
|
2657
|
+
...request.routeHints ?? {},
|
|
2658
|
+
requiredCapabilities: {
|
|
2659
|
+
...request.routeHints?.requiredCapabilities ?? {},
|
|
2660
|
+
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2661
|
+
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2662
|
+
...requiresImage ? { supportsImageInput: true } : {},
|
|
2663
|
+
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
} : request;
|
|
2667
|
+
return {
|
|
2668
|
+
requiresSchema,
|
|
2669
|
+
requiresClientTools,
|
|
2670
|
+
requiresImage,
|
|
2671
|
+
requiresFile,
|
|
2672
|
+
requiresAnyCapability,
|
|
2673
|
+
request: projected
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2337
2676
|
function normalizeRequestBase(request) {
|
|
2338
2677
|
if (request.messages.length === 0) {
|
|
2339
2678
|
throw new AiConnectError("validation_error", "Unified generate/stream requests must include at least one message.");
|
|
@@ -2542,7 +2881,10 @@ function summarizeRoute(route) {
|
|
|
2542
2881
|
model: route.model,
|
|
2543
2882
|
handlerKey: route.handlerKey,
|
|
2544
2883
|
profileId: route.profileId,
|
|
2545
|
-
|
|
2884
|
+
// SYS-3 / C1a: never emit a raw baseUrl into a wide event — a token embedded in
|
|
2885
|
+
// userinfo (`https://user:secret@host`) or the query string would leak verbatim,
|
|
2886
|
+
// unlike the secret-safe toPublicRoute projection. Strip credentials first.
|
|
2887
|
+
...route.transport.baseUrl ? { baseUrl: redactBaseUrl(route.transport.baseUrl) } : {}
|
|
2546
2888
|
};
|
|
2547
2889
|
}
|
|
2548
2890
|
function summarizeFiles(files) {
|
|
@@ -2853,21 +3195,40 @@ function createClient(configOrInput, options = {}) {
|
|
|
2853
3195
|
for (const toolCall of toolCalls) {
|
|
2854
3196
|
const tool = registry.get(toolCall.name);
|
|
2855
3197
|
if (!tool) {
|
|
2856
|
-
throw new
|
|
2857
|
-
|
|
2858
|
-
|
|
3198
|
+
throw new ClientToolExecutionError(
|
|
3199
|
+
new AiConnectError(
|
|
3200
|
+
"validation_error",
|
|
3201
|
+
`No client tool handler is registered for "${toolCall.name}".`
|
|
3202
|
+
)
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
if (abort.signal.aborted) {
|
|
3206
|
+
throw mapAbortError(abort.reason);
|
|
3207
|
+
}
|
|
3208
|
+
let execution;
|
|
3209
|
+
try {
|
|
3210
|
+
execution = normalizeToolResult(
|
|
3211
|
+
await tool.execute(toolCall.arguments, {
|
|
3212
|
+
route,
|
|
3213
|
+
request,
|
|
3214
|
+
runtime,
|
|
3215
|
+
toolCall,
|
|
3216
|
+
messages,
|
|
3217
|
+
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
3218
|
+
})
|
|
3219
|
+
);
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
if (abort.signal.aborted) {
|
|
3222
|
+
throw mapAbortError(abort.reason);
|
|
3223
|
+
}
|
|
3224
|
+
throw new ClientToolExecutionError(
|
|
3225
|
+
new AiConnectError(
|
|
3226
|
+
"validation_error",
|
|
3227
|
+
`Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
3228
|
+
{ cause: error }
|
|
3229
|
+
)
|
|
2859
3230
|
);
|
|
2860
3231
|
}
|
|
2861
|
-
const execution = normalizeToolResult(
|
|
2862
|
-
await tool.execute(toolCall.arguments, {
|
|
2863
|
-
route,
|
|
2864
|
-
request,
|
|
2865
|
-
runtime,
|
|
2866
|
-
toolCall,
|
|
2867
|
-
messages,
|
|
2868
|
-
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
2869
|
-
})
|
|
2870
|
-
);
|
|
2871
3232
|
toolMessages.push({
|
|
2872
3233
|
role: "tool",
|
|
2873
3234
|
content: execution.content,
|
|
@@ -2881,28 +3242,33 @@ function createClient(configOrInput, options = {}) {
|
|
|
2881
3242
|
if (abort.signal.aborted) {
|
|
2882
3243
|
throw mapAbortError(abort.reason);
|
|
2883
3244
|
}
|
|
2884
|
-
output = await
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3245
|
+
output = await runWithAbortRace(
|
|
3246
|
+
handler({
|
|
3247
|
+
route,
|
|
3248
|
+
request: {
|
|
3249
|
+
...request,
|
|
3250
|
+
messages,
|
|
3251
|
+
attachments: []
|
|
3252
|
+
},
|
|
3253
|
+
runtime,
|
|
3254
|
+
attempt: baseAttempt,
|
|
3255
|
+
abort,
|
|
3256
|
+
telemetry: {
|
|
3257
|
+
captureTransport(summary) {
|
|
3258
|
+
transportSummary2 = summary;
|
|
3259
|
+
}
|
|
2897
3260
|
}
|
|
2898
|
-
}
|
|
2899
|
-
|
|
3261
|
+
}),
|
|
3262
|
+
abort
|
|
3263
|
+
);
|
|
2900
3264
|
warnings.push(...output.warnings ?? []);
|
|
2901
3265
|
usage = mergeUsage(usage, output.usage);
|
|
2902
3266
|
}
|
|
2903
|
-
throw new
|
|
2904
|
-
|
|
2905
|
-
|
|
3267
|
+
throw new ClientToolExecutionError(
|
|
3268
|
+
new AiConnectError(
|
|
3269
|
+
"validation_error",
|
|
3270
|
+
"clientTools exceeded the maximum tool-call loop depth of 8."
|
|
3271
|
+
)
|
|
2906
3272
|
);
|
|
2907
3273
|
}
|
|
2908
3274
|
async function emitWideEvent(event) {
|
|
@@ -2933,26 +3299,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
2933
3299
|
async function executeGenerateOperation(normalizedRequest, operationName, abort, seed = {}) {
|
|
2934
3300
|
const startedAt = seed.startMs ?? nowMs(runtime);
|
|
2935
3301
|
const requestId = seed.requestId ?? resolveEventRequestId(normalizedRequest.logContext);
|
|
2936
|
-
const
|
|
2937
|
-
const
|
|
2938
|
-
const requiresImage = requiresImageInput(normalizedRequest);
|
|
2939
|
-
const requiresFile = requiresFileUpload(normalizedRequest);
|
|
2940
|
-
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2941
|
-
const resolvedCandidates = router.resolveCandidates(
|
|
2942
|
-
requiresAnyCapability ? {
|
|
2943
|
-
...normalizedRequest,
|
|
2944
|
-
routeHints: {
|
|
2945
|
-
...normalizedRequest.routeHints ?? {},
|
|
2946
|
-
requiredCapabilities: {
|
|
2947
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
2948
|
-
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2949
|
-
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2950
|
-
...requiresImage ? { supportsImageInput: true } : {},
|
|
2951
|
-
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
} : normalizedRequest
|
|
2955
|
-
);
|
|
3302
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
3303
|
+
const resolvedCandidates = router.resolveCandidates(capabilityScopedRequest);
|
|
2956
3304
|
const candidates = seed.candidates ?? resolvedCandidates.map(summarizeRoute);
|
|
2957
3305
|
const requestSummary = summarizeRequest(normalizedRequest, candidates);
|
|
2958
3306
|
if (resolvedCandidates.length === 0) {
|
|
@@ -3040,18 +3388,21 @@ function createClient(configOrInput, options = {}) {
|
|
|
3040
3388
|
const attemptStartedAt = nowMs(runtime);
|
|
3041
3389
|
let transportSummary2;
|
|
3042
3390
|
try {
|
|
3043
|
-
let output = await
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3391
|
+
let output = await runWithAbortRace(
|
|
3392
|
+
handler.generate({
|
|
3393
|
+
route,
|
|
3394
|
+
request: routeRequest,
|
|
3395
|
+
runtime,
|
|
3396
|
+
attempt: attempts.length + 1,
|
|
3397
|
+
abort,
|
|
3398
|
+
telemetry: {
|
|
3399
|
+
captureTransport(summary) {
|
|
3400
|
+
transportSummary2 = summary;
|
|
3401
|
+
}
|
|
3052
3402
|
}
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3403
|
+
}),
|
|
3404
|
+
abort
|
|
3405
|
+
);
|
|
3055
3406
|
if (routeRequest.clientTools?.length && output.toolCalls?.length) {
|
|
3056
3407
|
const loopResult = await executeClientToolLoop(
|
|
3057
3408
|
route,
|
|
@@ -3066,6 +3417,9 @@ function createClient(configOrInput, options = {}) {
|
|
|
3066
3417
|
transportSummary2 = loopResult.transportSummary;
|
|
3067
3418
|
}
|
|
3068
3419
|
}
|
|
3420
|
+
if (abort.signal.aborted) {
|
|
3421
|
+
throw mapAbortError(abort.reason);
|
|
3422
|
+
}
|
|
3069
3423
|
wideAttempts.push({
|
|
3070
3424
|
index: wideAttempts.length + 1,
|
|
3071
3425
|
route: routeSummary,
|
|
@@ -3136,6 +3490,49 @@ function createClient(configOrInput, options = {}) {
|
|
|
3136
3490
|
routeId: route.id
|
|
3137
3491
|
});
|
|
3138
3492
|
}
|
|
3493
|
+
if (error instanceof ClientToolExecutionError) {
|
|
3494
|
+
const toolError = error.aiError;
|
|
3495
|
+
attempts.push({
|
|
3496
|
+
routeId: route.id,
|
|
3497
|
+
handlerKey: route.handlerKey,
|
|
3498
|
+
errorCode: toolError.code,
|
|
3499
|
+
message: toolError.message
|
|
3500
|
+
});
|
|
3501
|
+
wideAttempts.push({
|
|
3502
|
+
index: wideAttempts.length + 1,
|
|
3503
|
+
route: routeSummary,
|
|
3504
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
3505
|
+
outcome: "error",
|
|
3506
|
+
retryCount,
|
|
3507
|
+
errorCode: toolError.code,
|
|
3508
|
+
message: toolError.message,
|
|
3509
|
+
...transportSummary2 ? { transport: transportSummary2 } : {}
|
|
3510
|
+
});
|
|
3511
|
+
await emitWideEvent({
|
|
3512
|
+
timestamp: isoTimestamp(startedAt),
|
|
3513
|
+
event: "ai_connect.operation",
|
|
3514
|
+
requestId,
|
|
3515
|
+
operationName,
|
|
3516
|
+
operation: normalizedRequest.operation,
|
|
3517
|
+
runtime: runtime.kind,
|
|
3518
|
+
outcome: "error",
|
|
3519
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
3520
|
+
candidates,
|
|
3521
|
+
attempts: wideAttempts,
|
|
3522
|
+
request: requestSummary,
|
|
3523
|
+
error: {
|
|
3524
|
+
code: toolError.code,
|
|
3525
|
+
message: toolError.message,
|
|
3526
|
+
routeId: route.id
|
|
3527
|
+
},
|
|
3528
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
3529
|
+
});
|
|
3530
|
+
throw new AiConnectError(toolError.code, toolError.message, {
|
|
3531
|
+
...toolError.details,
|
|
3532
|
+
attempts,
|
|
3533
|
+
routeId: route.id
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3139
3536
|
const normalizedError = toAiConnectError(error);
|
|
3140
3537
|
const actions = config.routing.fallback.on[normalizedError.code] ?? [];
|
|
3141
3538
|
attempts.push({
|
|
@@ -3380,30 +3777,16 @@ function createClient(configOrInput, options = {}) {
|
|
|
3380
3777
|
return report;
|
|
3381
3778
|
}
|
|
3382
3779
|
async function runPingWithAbort(route, handler, abort) {
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3780
|
+
return runWithAbortRace(
|
|
3781
|
+
handler({
|
|
3782
|
+
route,
|
|
3783
|
+
request: buildPingRequest(),
|
|
3784
|
+
runtime,
|
|
3785
|
+
attempt: 1,
|
|
3786
|
+
abort
|
|
3787
|
+
}),
|
|
3391
3788
|
abort
|
|
3392
|
-
|
|
3393
|
-
let onAbort;
|
|
3394
|
-
const abortPromise = new Promise((_, reject) => {
|
|
3395
|
-
onAbort = () => reject(mapAbortError(abort.reason));
|
|
3396
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
3397
|
-
});
|
|
3398
|
-
try {
|
|
3399
|
-
return await Promise.race([handlerPromise, abortPromise]);
|
|
3400
|
-
} finally {
|
|
3401
|
-
if (onAbort) {
|
|
3402
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
3403
|
-
}
|
|
3404
|
-
void Promise.resolve(handlerPromise).catch(() => {
|
|
3405
|
-
});
|
|
3406
|
-
}
|
|
3789
|
+
);
|
|
3407
3790
|
}
|
|
3408
3791
|
async function checkEndpointReachable(route, handler, abort) {
|
|
3409
3792
|
if (abort.signal.aborted) {
|
|
@@ -3971,19 +4354,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
3971
4354
|
release = await limiter.acquire(abort.signal);
|
|
3972
4355
|
const selectedModel = await resolveSelectedModel(preparedRequest);
|
|
3973
4356
|
const normalizedRequest = withSelectedModel(preparedRequest, selectedModel);
|
|
3974
|
-
const
|
|
3975
|
-
const candidates = router.resolveCandidates(
|
|
3976
|
-
requiresSchema ? {
|
|
3977
|
-
...normalizedRequest,
|
|
3978
|
-
routeHints: {
|
|
3979
|
-
...normalizedRequest.routeHints ?? {},
|
|
3980
|
-
requiredCapabilities: {
|
|
3981
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
3982
|
-
supportsToolSchema: true
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
} : normalizedRequest
|
|
3986
|
-
);
|
|
4357
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
4358
|
+
const candidates = router.resolveCandidates(capabilityScopedRequest);
|
|
3987
4359
|
const startedAt = nowMs(runtime);
|
|
3988
4360
|
const requestId = resolveEventRequestId(normalizedRequest.logContext);
|
|
3989
4361
|
const candidateSummaries = candidates.map(summarizeRoute);
|
|
@@ -3991,7 +4363,10 @@ function createClient(configOrInput, options = {}) {
|
|
|
3991
4363
|
const attempts = [];
|
|
3992
4364
|
const wideAttempts = [];
|
|
3993
4365
|
if (candidates.length === 0) {
|
|
3994
|
-
const error =
|
|
4366
|
+
const error = requiresImage || requiresFile ? new AiConnectError(
|
|
4367
|
+
"unsupported_capability",
|
|
4368
|
+
`No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
|
|
4369
|
+
) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
|
|
3995
4370
|
await emitWideEvent({
|
|
3996
4371
|
timestamp: isoTimestamp(startedAt),
|
|
3997
4372
|
event: "ai_connect.operation",
|
|
@@ -4449,12 +4824,32 @@ function trimTrailingSlash(value) {
|
|
|
4449
4824
|
function appendPath(baseUrl, suffix) {
|
|
4450
4825
|
return `${trimTrailingSlash(baseUrl)}/${suffix.replace(/^\/+/, "")}`;
|
|
4451
4826
|
}
|
|
4452
|
-
function
|
|
4453
|
-
|
|
4827
|
+
function isAbortError(error) {
|
|
4828
|
+
if (error instanceof AiConnectError) {
|
|
4829
|
+
return error.code === "aborted";
|
|
4830
|
+
}
|
|
4831
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
4832
|
+
}
|
|
4833
|
+
function attachPartsToLastUserTurn(conversation, createEmptyUserTurn) {
|
|
4834
|
+
let targetIndex = -1;
|
|
4835
|
+
for (let index = conversation.length - 1; index >= 0; index -= 1) {
|
|
4836
|
+
if (conversation[index]?.role === "user") {
|
|
4837
|
+
targetIndex = index;
|
|
4838
|
+
break;
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
if (targetIndex === -1) {
|
|
4842
|
+
conversation.push(createEmptyUserTurn());
|
|
4843
|
+
targetIndex = conversation.length - 1;
|
|
4844
|
+
}
|
|
4845
|
+
return conversation[targetIndex];
|
|
4846
|
+
}
|
|
4847
|
+
function resolveOpenAiEndpoint(route, operation) {
|
|
4848
|
+
const configured = route.transport.baseUrl?.trim();
|
|
4454
4849
|
if (!configured) {
|
|
4455
4850
|
return operation === "image" ? "https://api.openai.com/v1/images/generations" : "https://api.openai.com/v1/chat/completions";
|
|
4456
4851
|
}
|
|
4457
|
-
if (configured.endsWith("/chat/completions") || configured.endsWith("/images/generations")) {
|
|
4852
|
+
if (operation === "text" && configured.endsWith("/chat/completions") || operation === "image" && configured.endsWith("/images/generations")) {
|
|
4458
4853
|
return configured;
|
|
4459
4854
|
}
|
|
4460
4855
|
return appendPath(
|
|
@@ -5285,7 +5680,7 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5285
5680
|
}
|
|
5286
5681
|
try {
|
|
5287
5682
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5288
|
-
const root = base ? base.replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5683
|
+
const root = base ? trimTrailingSlash(base).replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5289
5684
|
const endpoint = appendPath(root, "/files");
|
|
5290
5685
|
const form = new FormData();
|
|
5291
5686
|
form.append(
|
|
@@ -5309,7 +5704,10 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5309
5704
|
}
|
|
5310
5705
|
const id = recordOf(data).id;
|
|
5311
5706
|
return typeof id === "string" ? id : void 0;
|
|
5312
|
-
} catch {
|
|
5707
|
+
} catch (error) {
|
|
5708
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5709
|
+
throw error;
|
|
5710
|
+
}
|
|
5313
5711
|
return void 0;
|
|
5314
5712
|
}
|
|
5315
5713
|
}
|
|
@@ -5342,7 +5740,10 @@ async function uploadOpenAiFile(payload, uploader) {
|
|
|
5342
5740
|
}
|
|
5343
5741
|
const id = recordOf(data).id;
|
|
5344
5742
|
return typeof id === "string" ? id : void 0;
|
|
5345
|
-
} catch {
|
|
5743
|
+
} catch (error) {
|
|
5744
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5745
|
+
throw error;
|
|
5746
|
+
}
|
|
5346
5747
|
return void 0;
|
|
5347
5748
|
}
|
|
5348
5749
|
}
|
|
@@ -5352,10 +5753,11 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5352
5753
|
}
|
|
5353
5754
|
try {
|
|
5354
5755
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5355
|
-
const
|
|
5356
|
-
|
|
5756
|
+
const root = base ? trimTrailingSlash(base.replace(/\/v1beta\/models.*$/, "/v1beta")) : void 0;
|
|
5757
|
+
const endpoint = root ? appendPath(
|
|
5758
|
+
root.endsWith("/v1beta") ? `${root.slice(0, -"/v1beta".length)}/upload/v1beta` : `${root}/upload`,
|
|
5357
5759
|
"/files"
|
|
5358
|
-
)
|
|
5760
|
+
) : "https://generativelanguage.googleapis.com/upload/v1beta/files";
|
|
5359
5761
|
const response = await uploader.fetchImpl(endpoint, {
|
|
5360
5762
|
method: "POST",
|
|
5361
5763
|
headers: {
|
|
@@ -5374,7 +5776,10 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5374
5776
|
const file = recordOf(recordOf(data).file);
|
|
5375
5777
|
const uri = file.uri ?? recordOf(data).uri;
|
|
5376
5778
|
return typeof uri === "string" ? uri : void 0;
|
|
5377
|
-
} catch {
|
|
5779
|
+
} catch (error) {
|
|
5780
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5781
|
+
throw error;
|
|
5782
|
+
}
|
|
5378
5783
|
return void 0;
|
|
5379
5784
|
}
|
|
5380
5785
|
}
|
|
@@ -5572,21 +5977,10 @@ function attachOpenAiParts(conversation, parts) {
|
|
|
5572
5977
|
if (parts.length === 0) {
|
|
5573
5978
|
return conversation;
|
|
5574
5979
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
break;
|
|
5580
|
-
}
|
|
5581
|
-
}
|
|
5582
|
-
if (targetIndex === -1) {
|
|
5583
|
-
conversation.push({
|
|
5584
|
-
role: "user",
|
|
5585
|
-
content: ""
|
|
5586
|
-
});
|
|
5587
|
-
targetIndex = conversation.length - 1;
|
|
5588
|
-
}
|
|
5589
|
-
const target = conversation[targetIndex];
|
|
5980
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
5981
|
+
role: "user",
|
|
5982
|
+
content: ""
|
|
5983
|
+
}));
|
|
5590
5984
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5591
5985
|
target.content = [...baseParts, ...parts];
|
|
5592
5986
|
return conversation;
|
|
@@ -5672,21 +6066,10 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5672
6066
|
}
|
|
5673
6067
|
const attachmentParts = await anthropicAttachmentParts(attachments, uploader);
|
|
5674
6068
|
if (attachmentParts.length > 0) {
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
break;
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
if (targetIndex === -1) {
|
|
5683
|
-
conversation.push({
|
|
5684
|
-
role: "user",
|
|
5685
|
-
content: ""
|
|
5686
|
-
});
|
|
5687
|
-
targetIndex = conversation.length - 1;
|
|
5688
|
-
}
|
|
5689
|
-
const target = conversation[targetIndex];
|
|
6069
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6070
|
+
role: "user",
|
|
6071
|
+
content: ""
|
|
6072
|
+
}));
|
|
5690
6073
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5691
6074
|
target.content = [...baseParts, ...attachmentParts];
|
|
5692
6075
|
}
|
|
@@ -5697,12 +6080,27 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5697
6080
|
}
|
|
5698
6081
|
async function geminiRequest(messages, attachments, uploader) {
|
|
5699
6082
|
const system = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n");
|
|
6083
|
+
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
6084
|
+
for (const message of messages) {
|
|
6085
|
+
if (hasAssistantToolCalls(message)) {
|
|
6086
|
+
for (const call of message.toolCalls) {
|
|
6087
|
+
toolNameByCallId.set(call.id, call.name);
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
5700
6091
|
const contents = messages.filter((message) => message.role !== "system").reduce(
|
|
5701
6092
|
(conversation, message) => {
|
|
5702
6093
|
if (isToolMessage(message)) {
|
|
6094
|
+
const resolvedName = message.toolName ?? toolNameByCallId.get(message.toolCallId);
|
|
6095
|
+
if (!resolvedName) {
|
|
6096
|
+
throw new AiConnectError(
|
|
6097
|
+
"validation_error",
|
|
6098
|
+
`Gemini functionResponse requires a tool name: the tool message for toolCallId "${message.toolCallId}" has no toolName and no matching assistant toolCall was found.`
|
|
6099
|
+
);
|
|
6100
|
+
}
|
|
5703
6101
|
const response = {
|
|
5704
6102
|
functionResponse: {
|
|
5705
|
-
name:
|
|
6103
|
+
name: resolvedName,
|
|
5706
6104
|
response: isRecord(message.data) ? message.data : {
|
|
5707
6105
|
content: message.content,
|
|
5708
6106
|
...message.isError !== void 0 ? { isError: message.isError } : {}
|
|
@@ -5749,21 +6147,11 @@ async function geminiRequest(messages, attachments, uploader) {
|
|
|
5749
6147
|
);
|
|
5750
6148
|
const attachmentParts = await geminiAttachmentParts(attachments, uploader);
|
|
5751
6149
|
if (attachmentParts.length > 0) {
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
}
|
|
5758
|
-
}
|
|
5759
|
-
if (targetIndex === -1) {
|
|
5760
|
-
contents.push({
|
|
5761
|
-
role: "user",
|
|
5762
|
-
parts: []
|
|
5763
|
-
});
|
|
5764
|
-
targetIndex = contents.length - 1;
|
|
5765
|
-
}
|
|
5766
|
-
contents[targetIndex].parts.push(...attachmentParts);
|
|
6150
|
+
const target = attachPartsToLastUserTurn(contents, () => ({
|
|
6151
|
+
role: "user",
|
|
6152
|
+
parts: []
|
|
6153
|
+
}));
|
|
6154
|
+
target.parts.push(...attachmentParts);
|
|
5767
6155
|
}
|
|
5768
6156
|
return {
|
|
5769
6157
|
...system ? {
|
|
@@ -5813,6 +6201,22 @@ function extractThoughts(value) {
|
|
|
5813
6201
|
const joined = thoughts.join("\n").trim();
|
|
5814
6202
|
return joined.length > 0 ? joined : void 0;
|
|
5815
6203
|
}
|
|
6204
|
+
function extractThinking(value) {
|
|
6205
|
+
if (!Array.isArray(value)) {
|
|
6206
|
+
return void 0;
|
|
6207
|
+
}
|
|
6208
|
+
const thinking = value.flatMap((part) => {
|
|
6209
|
+
if (part && typeof part === "object" && part.type === "thinking") {
|
|
6210
|
+
const candidate = part.thinking;
|
|
6211
|
+
if (typeof candidate === "string") {
|
|
6212
|
+
return [candidate];
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
return [];
|
|
6216
|
+
});
|
|
6217
|
+
const joined = thinking.join("\n").trim();
|
|
6218
|
+
return joined.length > 0 ? joined : void 0;
|
|
6219
|
+
}
|
|
5816
6220
|
function imageAttachmentsFromPayload(payload, ...sources) {
|
|
5817
6221
|
return extractImagePayloads(payload, ...sources).map(
|
|
5818
6222
|
(item) => preparePortableFile(item)
|
|
@@ -5900,6 +6304,10 @@ function buildOpenAiChatBody(context, messages, parameters, responseFormat, opts
|
|
|
5900
6304
|
model: context.route.model,
|
|
5901
6305
|
messages,
|
|
5902
6306
|
stream: opts.stream,
|
|
6307
|
+
// Ask OpenAI-compatible streaming routes for the trailing usage chunk; without
|
|
6308
|
+
// `stream_options.include_usage` the SSE stream carries no `usage` block and
|
|
6309
|
+
// `runOpenAiStream` would always report `usage: undefined` (H5).
|
|
6310
|
+
...opts.stream ? { stream_options: { include_usage: true } } : {},
|
|
5903
6311
|
...parameters?.maxTokens !== void 0 ? { max_tokens: parameters.maxTokens } : {},
|
|
5904
6312
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
5905
6313
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6054,6 +6462,11 @@ async function* runOpenAiStream(fetchImpl, context) {
|
|
|
6054
6462
|
}
|
|
6055
6463
|
const response = outcome.response;
|
|
6056
6464
|
const streamWarnings = [...uploaderWarnings];
|
|
6465
|
+
if (parameters?.candidateCount && parameters.candidateCount > 1) {
|
|
6466
|
+
streamWarnings.push(
|
|
6467
|
+
"candidateCount > 1 is forwarded to OpenAI-compatible text routes, but the normalized result returns only the first choice."
|
|
6468
|
+
);
|
|
6469
|
+
}
|
|
6057
6470
|
if (droppedResponseFormat) {
|
|
6058
6471
|
streamWarnings.push(
|
|
6059
6472
|
"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."
|
|
@@ -6199,8 +6612,7 @@ function buildOpenAiStreamResult(context, text, toolCalls, usage, warnings) {
|
|
|
6199
6612
|
} : {}
|
|
6200
6613
|
};
|
|
6201
6614
|
}
|
|
6202
|
-
async function
|
|
6203
|
-
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6615
|
+
async function runProviderImage(fetchImpl, context, opts) {
|
|
6204
6616
|
const referenceImages = imageReferenceFiles(context);
|
|
6205
6617
|
const bundle = buildImagePromptBundle({
|
|
6206
6618
|
request: context.request,
|
|
@@ -6209,10 +6621,10 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6209
6621
|
});
|
|
6210
6622
|
const baseFields = buildImageRequestFields(context, bundle);
|
|
6211
6623
|
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6212
|
-
const endpoint = useEditEndpoint ?
|
|
6624
|
+
const endpoint = useEditEndpoint ? opts.resolveEditEndpoint(context.route) : opts.resolveEndpoint(context.route);
|
|
6213
6625
|
transportSummary(
|
|
6214
6626
|
context,
|
|
6215
|
-
useEditEndpoint ?
|
|
6627
|
+
useEditEndpoint ? `${opts.providerLabel}-image-edit` : `${opts.providerLabel}-image`,
|
|
6216
6628
|
endpoint,
|
|
6217
6629
|
"POST",
|
|
6218
6630
|
false,
|
|
@@ -6225,23 +6637,21 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6225
6637
|
);
|
|
6226
6638
|
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6227
6639
|
method: "POST",
|
|
6228
|
-
headers: {
|
|
6229
|
-
authorization: `Bearer ${apiKey}`
|
|
6230
|
-
},
|
|
6640
|
+
headers: { ...opts.authHeaders },
|
|
6231
6641
|
body: await buildImageEditFormData(context, baseFields),
|
|
6232
6642
|
signal: context.abort.signal
|
|
6233
6643
|
} : {
|
|
6234
6644
|
method: "POST",
|
|
6235
6645
|
headers: {
|
|
6236
6646
|
"content-type": "application/json",
|
|
6237
|
-
|
|
6647
|
+
...opts.authHeaders
|
|
6238
6648
|
},
|
|
6239
6649
|
body: JSON.stringify(baseFields),
|
|
6240
6650
|
signal: context.abort.signal
|
|
6241
6651
|
});
|
|
6242
6652
|
const payload = await readPayload(response);
|
|
6243
6653
|
if (!response.ok) {
|
|
6244
|
-
throw classifyApiError(
|
|
6654
|
+
throw classifyApiError(opts.classifyProvider, response, payload);
|
|
6245
6655
|
}
|
|
6246
6656
|
return {
|
|
6247
6657
|
data: {
|
|
@@ -6251,6 +6661,16 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6251
6661
|
attachments: imageAttachmentsFromPayload(payload)
|
|
6252
6662
|
};
|
|
6253
6663
|
}
|
|
6664
|
+
async function runOpenAiImage(fetchImpl, context) {
|
|
6665
|
+
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6666
|
+
return runProviderImage(fetchImpl, context, {
|
|
6667
|
+
resolveEndpoint: (route) => resolveOpenAiEndpoint(route, "image"),
|
|
6668
|
+
resolveEditEndpoint: (route) => resolveOpenAiImageEditEndpoint(route),
|
|
6669
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6670
|
+
providerLabel: "openai",
|
|
6671
|
+
classifyProvider: "openai"
|
|
6672
|
+
});
|
|
6673
|
+
}
|
|
6254
6674
|
async function runAnthropic(fetchImpl, context) {
|
|
6255
6675
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6256
6676
|
const uploaderWarnings = [];
|
|
@@ -6262,6 +6682,8 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6262
6682
|
const parameters = context.request.parameters;
|
|
6263
6683
|
const requestBody = {
|
|
6264
6684
|
model: context.route.model,
|
|
6685
|
+
// Anthropic REQUIRES `max_tokens` (unlike OpenAI/Gemini, where it is optional), so a
|
|
6686
|
+
// provider-specific default of 4096 is applied when the caller omits `maxTokens`.
|
|
6265
6687
|
max_tokens: parameters?.maxTokens ?? 4096,
|
|
6266
6688
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
6267
6689
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6291,8 +6713,12 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6291
6713
|
throw classifyApiError("anthropic", response, payload);
|
|
6292
6714
|
}
|
|
6293
6715
|
const data = payload;
|
|
6716
|
+
const anthropicReasoning = extractThinking(data.content);
|
|
6294
6717
|
return {
|
|
6295
6718
|
text: extractText(data.content),
|
|
6719
|
+
// Anthropic extended-thinking blocks, separated from the answer (consumers route this
|
|
6720
|
+
// to a thinking UI), mirroring the Gemini `reasoning` path.
|
|
6721
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
|
|
6296
6722
|
data,
|
|
6297
6723
|
...anthropicToolCallsFromPayload(data).length > 0 ? { toolCalls: anthropicToolCallsFromPayload(data) } : {},
|
|
6298
6724
|
...uploaderWarnings.length > 0 ? { warnings: uploaderWarnings } : {},
|
|
@@ -6387,55 +6813,13 @@ async function runGemini(fetchImpl, context) {
|
|
|
6387
6813
|
}
|
|
6388
6814
|
async function runGeminiImage(fetchImpl, context) {
|
|
6389
6815
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6816
|
+
return runProviderImage(fetchImpl, context, {
|
|
6817
|
+
resolveEndpoint: (route) => resolveGeminiImageEndpoint(route),
|
|
6818
|
+
resolveEditEndpoint: (route) => resolveGeminiImageEditEndpoint(route),
|
|
6819
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6820
|
+
providerLabel: "gemini",
|
|
6821
|
+
classifyProvider: context.route.provider
|
|
6395
6822
|
});
|
|
6396
|
-
const baseFields = buildImageRequestFields(context, bundle);
|
|
6397
|
-
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6398
|
-
const endpoint = useEditEndpoint ? resolveGeminiImageEditEndpoint(context.route) : resolveGeminiImageEndpoint(context.route);
|
|
6399
|
-
transportSummary(
|
|
6400
|
-
context,
|
|
6401
|
-
useEditEndpoint ? "gemini-image-edit" : "gemini-image",
|
|
6402
|
-
endpoint,
|
|
6403
|
-
"POST",
|
|
6404
|
-
false,
|
|
6405
|
-
{
|
|
6406
|
-
...baseFields,
|
|
6407
|
-
source_image: Boolean(context.request.image?.sourceImage),
|
|
6408
|
-
mask: Boolean(context.request.image?.mask),
|
|
6409
|
-
reference_image_count: referenceImages.length
|
|
6410
|
-
}
|
|
6411
|
-
);
|
|
6412
|
-
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6413
|
-
method: "POST",
|
|
6414
|
-
headers: {
|
|
6415
|
-
authorization: `Bearer ${apiKey}`
|
|
6416
|
-
},
|
|
6417
|
-
body: await buildImageEditFormData(context, baseFields),
|
|
6418
|
-
signal: context.abort.signal
|
|
6419
|
-
} : {
|
|
6420
|
-
method: "POST",
|
|
6421
|
-
headers: {
|
|
6422
|
-
"content-type": "application/json",
|
|
6423
|
-
authorization: `Bearer ${apiKey}`
|
|
6424
|
-
},
|
|
6425
|
-
body: JSON.stringify(baseFields),
|
|
6426
|
-
signal: context.abort.signal
|
|
6427
|
-
});
|
|
6428
|
-
const payload = await readPayload(response);
|
|
6429
|
-
if (!response.ok) {
|
|
6430
|
-
throw classifyApiError(context.route.provider, response, payload);
|
|
6431
|
-
}
|
|
6432
|
-
return {
|
|
6433
|
-
data: {
|
|
6434
|
-
request: bundle,
|
|
6435
|
-
response: payload
|
|
6436
|
-
},
|
|
6437
|
-
attachments: imageAttachmentsFromPayload(payload)
|
|
6438
|
-
};
|
|
6439
6823
|
}
|
|
6440
6824
|
function verifyApiRoute(route, runtime) {
|
|
6441
6825
|
resolveApiKey(route, runtime);
|
|
@@ -6512,9 +6896,118 @@ var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
|
|
|
6512
6896
|
"opencode:opencode-acp": "opencode acp"
|
|
6513
6897
|
};
|
|
6514
6898
|
|
|
6899
|
+
// src/transport-runtime.ts
|
|
6900
|
+
var MINIMAL_ENV_KEYS = [
|
|
6901
|
+
"PATH",
|
|
6902
|
+
"HOME",
|
|
6903
|
+
"USERPROFILE",
|
|
6904
|
+
"HTTPS_PROXY",
|
|
6905
|
+
"HTTP_PROXY",
|
|
6906
|
+
"NO_PROXY",
|
|
6907
|
+
"https_proxy",
|
|
6908
|
+
"http_proxy",
|
|
6909
|
+
"no_proxy",
|
|
6910
|
+
"NODE_EXTRA_CA_CERTS",
|
|
6911
|
+
"SSL_CERT_FILE",
|
|
6912
|
+
"SSL_CERT_DIR",
|
|
6913
|
+
"SystemRoot",
|
|
6914
|
+
"windir",
|
|
6915
|
+
"APPDATA",
|
|
6916
|
+
"LOCALAPPDATA",
|
|
6917
|
+
"TEMP",
|
|
6918
|
+
"TMP",
|
|
6919
|
+
"PATHEXT",
|
|
6920
|
+
"ComSpec",
|
|
6921
|
+
"NUMBER_OF_PROCESSORS"
|
|
6922
|
+
];
|
|
6923
|
+
function resolveChildEnv(opts = {}) {
|
|
6924
|
+
const extra = opts.extra ?? {};
|
|
6925
|
+
if (opts.envMode === "inherit") {
|
|
6926
|
+
const inherited = {};
|
|
6927
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
6928
|
+
if (typeof value === "string") {
|
|
6929
|
+
inherited[key] = value;
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6932
|
+
return { ...inherited, ...extra };
|
|
6933
|
+
}
|
|
6934
|
+
const minimal = {};
|
|
6935
|
+
const keys = /* @__PURE__ */ new Set([...MINIMAL_ENV_KEYS, ...opts.allowlist ?? []]);
|
|
6936
|
+
for (const key of keys) {
|
|
6937
|
+
const value = process.env[key];
|
|
6938
|
+
if (typeof value === "string") {
|
|
6939
|
+
minimal[key] = value;
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
return { ...minimal, ...extra };
|
|
6943
|
+
}
|
|
6944
|
+
function killProcessTree(child, signal, detached) {
|
|
6945
|
+
if (process.platform !== "win32" && detached && typeof child.pid === "number") {
|
|
6946
|
+
try {
|
|
6947
|
+
process.kill(-child.pid, signal);
|
|
6948
|
+
return;
|
|
6949
|
+
} catch {
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
try {
|
|
6953
|
+
child.kill(signal);
|
|
6954
|
+
} catch {
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6957
|
+
function resolveManagedCommand(key, optionsCommands, routeCommand, presetCommand) {
|
|
6958
|
+
const resolved = optionsCommands?.[key]?.trim() || routeCommand?.trim() || presetCommand?.trim();
|
|
6959
|
+
if (!resolved) {
|
|
6960
|
+
throw new AiConnectError(
|
|
6961
|
+
"validation_error",
|
|
6962
|
+
`No managed command is configured for "${key}".`
|
|
6963
|
+
);
|
|
6964
|
+
}
|
|
6965
|
+
return resolved;
|
|
6966
|
+
}
|
|
6967
|
+
var trackedChildren = /* @__PURE__ */ new Set();
|
|
6968
|
+
var exitHookRegistered = false;
|
|
6969
|
+
function killAllTrackedChildren() {
|
|
6970
|
+
for (const tracked of trackedChildren) {
|
|
6971
|
+
killProcessTree(tracked.child, "SIGKILL", tracked.detached);
|
|
6972
|
+
}
|
|
6973
|
+
trackedChildren.clear();
|
|
6974
|
+
}
|
|
6975
|
+
function ensureExitHook() {
|
|
6976
|
+
if (exitHookRegistered) {
|
|
6977
|
+
return;
|
|
6978
|
+
}
|
|
6979
|
+
exitHookRegistered = true;
|
|
6980
|
+
process.on("exit", () => {
|
|
6981
|
+
killAllTrackedChildren();
|
|
6982
|
+
});
|
|
6983
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
6984
|
+
const handler = () => {
|
|
6985
|
+
killAllTrackedChildren();
|
|
6986
|
+
process.removeListener(signal, handler);
|
|
6987
|
+
if (process.listenerCount(signal) === 0) {
|
|
6988
|
+
process.kill(process.pid, signal);
|
|
6989
|
+
}
|
|
6990
|
+
};
|
|
6991
|
+
process.on(signal, handler);
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
function trackChild(child, detached) {
|
|
6995
|
+
ensureExitHook();
|
|
6996
|
+
const entry = { child, detached };
|
|
6997
|
+
trackedChildren.add(entry);
|
|
6998
|
+
const untrack = () => {
|
|
6999
|
+
trackedChildren.delete(entry);
|
|
7000
|
+
};
|
|
7001
|
+
child.once("close", untrack);
|
|
7002
|
+
child.once("exit", untrack);
|
|
7003
|
+
return untrack;
|
|
7004
|
+
}
|
|
7005
|
+
|
|
6515
7006
|
// src/acp.ts
|
|
6516
7007
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
6517
7008
|
var DEFAULT_SESSION_CACHE_IDLE_TTL_MS = 6e4;
|
|
7009
|
+
var MAX_ACP_STDERR_CHARS = 64 * 1024;
|
|
7010
|
+
var MAX_ACP_STREAM_QUEUE = 1024;
|
|
6518
7011
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
6519
7012
|
[".png", "image/png"],
|
|
6520
7013
|
[".jpg", "image/jpeg"],
|
|
@@ -6803,24 +7296,29 @@ function prepareClaudeLaunchRuntime(base, launch) {
|
|
|
6803
7296
|
launch
|
|
6804
7297
|
};
|
|
6805
7298
|
}
|
|
7299
|
+
var ACP_LAUNCH_PREPARERS = {
|
|
7300
|
+
"opencode-acp": prepareOpenCodeLaunchRuntime,
|
|
7301
|
+
"codex-acp": prepareCodexLaunchRuntime,
|
|
7302
|
+
"claude-code-acp": prepareClaudeLaunchRuntime
|
|
7303
|
+
};
|
|
6806
7304
|
async function prepareAcpLaunchRuntime(route, options, commandLine, cwdOverride) {
|
|
6807
7305
|
const launch = normalizeLaunchOptions(route, options);
|
|
6808
7306
|
const base = {
|
|
6809
7307
|
commandLine,
|
|
6810
7308
|
cwd: path.resolve(cwdOverride ?? options?.cwd ?? process.cwd()),
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
}
|
|
7309
|
+
// SYS-2: minimal env by default — do NOT hand the host's full process.env
|
|
7310
|
+
// (other-provider keys, CI tokens) to third-party ACP agents. Extra env is
|
|
7311
|
+
// layered via options.env.
|
|
7312
|
+
env: resolveChildEnv({ extra: options?.env ?? {} })
|
|
6815
7313
|
};
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
if (route.transport.id === "codex-acp") {
|
|
6820
|
-
return prepareCodexLaunchRuntime(base, launch);
|
|
7314
|
+
const preparer = ACP_LAUNCH_PREPARERS[route.transport.id];
|
|
7315
|
+
if (preparer) {
|
|
7316
|
+
return await preparer(base, launch);
|
|
6821
7317
|
}
|
|
6822
|
-
if (
|
|
6823
|
-
|
|
7318
|
+
if (launch.contextMode !== "workspace" || launch.skillsMode !== "default") {
|
|
7319
|
+
console.warn(
|
|
7320
|
+
`[ai-connect] ACP transport "${route.transport.id}" has no launch-runtime preparer; requested contextMode/skillsMode isolation is not applied.`
|
|
7321
|
+
);
|
|
6824
7322
|
}
|
|
6825
7323
|
return {
|
|
6826
7324
|
...base,
|
|
@@ -6852,8 +7350,27 @@ function mimeTypeFromPath(filePath) {
|
|
|
6852
7350
|
function isTextPath(filePath) {
|
|
6853
7351
|
return TEXT_EXTENSIONS2.has(path.extname(filePath).toLowerCase());
|
|
6854
7352
|
}
|
|
6855
|
-
function
|
|
6856
|
-
|
|
7353
|
+
async function resolveRealPathTolerant(target) {
|
|
7354
|
+
try {
|
|
7355
|
+
return await fs.realpath(target);
|
|
7356
|
+
} catch {
|
|
7357
|
+
const parent = path.dirname(target);
|
|
7358
|
+
if (parent === target) {
|
|
7359
|
+
return target;
|
|
7360
|
+
}
|
|
7361
|
+
const realParent = await resolveRealPathTolerant(parent);
|
|
7362
|
+
return path.join(realParent, path.basename(target));
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
async function isWithinRoot(rootDir, targetPath) {
|
|
7366
|
+
let realRoot;
|
|
7367
|
+
try {
|
|
7368
|
+
realRoot = await fs.realpath(rootDir);
|
|
7369
|
+
} catch {
|
|
7370
|
+
realRoot = path.resolve(rootDir);
|
|
7371
|
+
}
|
|
7372
|
+
const realTarget = await resolveRealPathTolerant(path.resolve(targetPath));
|
|
7373
|
+
const relative = path.relative(realRoot, realTarget);
|
|
6857
7374
|
return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
6858
7375
|
}
|
|
6859
7376
|
function buildMessageTranscript(context) {
|
|
@@ -7280,46 +7797,58 @@ function extractTextChunk(update) {
|
|
|
7280
7797
|
}
|
|
7281
7798
|
return typeof content.text === "string" && content.text.length > 0 ? content.text : void 0;
|
|
7282
7799
|
}
|
|
7800
|
+
function isAuthErrorMessage(message) {
|
|
7801
|
+
return /unauthor|authentication|invalid[_\s-]?(api[_\s-]?key|credential|token)|forbidden|\b40[13]\b/i.test(
|
|
7802
|
+
message
|
|
7803
|
+
);
|
|
7804
|
+
}
|
|
7283
7805
|
function normalizeAcpError(error) {
|
|
7284
7806
|
if (error instanceof AiConnectError) {
|
|
7285
7807
|
return error;
|
|
7286
7808
|
}
|
|
7809
|
+
if (error instanceof Error) {
|
|
7810
|
+
const message = error.message || "ACP transport failed.";
|
|
7811
|
+
const errno = error.code;
|
|
7812
|
+
if (errno === "ENOENT" || errno === "EACCES" || errno === "ENOEXEC" || message.toLowerCase().includes("timed out")) {
|
|
7813
|
+
return new AiConnectError("local_harness_unavailable", message);
|
|
7814
|
+
}
|
|
7815
|
+
if (isAuthErrorMessage(message)) {
|
|
7816
|
+
return new AiConnectError("auth_error", message);
|
|
7817
|
+
}
|
|
7818
|
+
return new AiConnectError("temporary_unavailable", message);
|
|
7819
|
+
}
|
|
7287
7820
|
if (isRecord2(error)) {
|
|
7288
7821
|
const code = typeof error.code === "number" ? error.code : void 0;
|
|
7289
7822
|
const message = typeof error.message === "string" ? error.message : "ACP transport failed.";
|
|
7290
7823
|
if (code === -32601 || code === -32602) {
|
|
7291
7824
|
return new AiConnectError("not_supported", message);
|
|
7292
7825
|
}
|
|
7293
|
-
if (message
|
|
7826
|
+
if (isAuthErrorMessage(message)) {
|
|
7294
7827
|
return new AiConnectError("auth_error", message);
|
|
7295
7828
|
}
|
|
7296
7829
|
return new AiConnectError("temporary_unavailable", message);
|
|
7297
7830
|
}
|
|
7298
|
-
if (error instanceof Error) {
|
|
7299
|
-
const message = error.message;
|
|
7300
|
-
if ("code" in error && error.code === "ENOENT") {
|
|
7301
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7302
|
-
}
|
|
7303
|
-
if (message.toLowerCase().includes("timed out")) {
|
|
7304
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7305
|
-
}
|
|
7306
|
-
return new AiConnectError("temporary_unavailable", message);
|
|
7307
|
-
}
|
|
7308
7831
|
return new AiConnectError(
|
|
7309
7832
|
"temporary_unavailable",
|
|
7310
7833
|
"ACP transport failed."
|
|
7311
7834
|
);
|
|
7312
7835
|
}
|
|
7313
7836
|
function resolveAcpCommand(route, options) {
|
|
7314
|
-
const
|
|
7315
|
-
const
|
|
7316
|
-
|
|
7837
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
7838
|
+
const optionsOverride = options?.commands?.[key] ?? options?.commands?.[route.transport.id] ?? options?.commands?.[route.provider];
|
|
7839
|
+
try {
|
|
7840
|
+
return resolveManagedCommand(
|
|
7841
|
+
key,
|
|
7842
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
7843
|
+
route.transport.command,
|
|
7844
|
+
AI_CONNECT_DEFAULT_ACP_COMMANDS[key]
|
|
7845
|
+
);
|
|
7846
|
+
} catch {
|
|
7317
7847
|
throw new AiConnectError(
|
|
7318
7848
|
"validation_error",
|
|
7319
7849
|
`No ACP command is configured for route "${route.id}".`
|
|
7320
7850
|
);
|
|
7321
7851
|
}
|
|
7322
|
-
return resolved;
|
|
7323
7852
|
}
|
|
7324
7853
|
function resolveAcpExecutable(route, options) {
|
|
7325
7854
|
return splitCommandLine(resolveAcpCommand(route, options)).command;
|
|
@@ -7470,14 +7999,7 @@ async function settleClosePromise(closePromise, timeoutMs = 1500) {
|
|
|
7470
7999
|
]);
|
|
7471
8000
|
}
|
|
7472
8001
|
function killAcpProcess(child, signal) {
|
|
7473
|
-
|
|
7474
|
-
try {
|
|
7475
|
-
process.kill(-child.pid, signal);
|
|
7476
|
-
return;
|
|
7477
|
-
} catch {
|
|
7478
|
-
}
|
|
7479
|
-
}
|
|
7480
|
-
child.kill(signal);
|
|
8002
|
+
killProcessTree(child, signal, process.platform !== "win32");
|
|
7481
8003
|
}
|
|
7482
8004
|
var AcpConnection = class {
|
|
7483
8005
|
commandLine;
|
|
@@ -7609,6 +8131,9 @@ var AcpConnection = class {
|
|
|
7609
8131
|
if (stderrSlice.length > 0) {
|
|
7610
8132
|
warnings.push(stderrSlice);
|
|
7611
8133
|
}
|
|
8134
|
+
if (this.stderr.length > MAX_ACP_STDERR_CHARS) {
|
|
8135
|
+
this.stderr = this.stderr.slice(-MAX_ACP_STDERR_CHARS);
|
|
8136
|
+
}
|
|
7612
8137
|
const text = this.activePrompt.text.trim();
|
|
7613
8138
|
if (this.failOnHarnessOnlyTurn && this.activePrompt.harnessMarkers.length > 0 && text.length === 0) {
|
|
7614
8139
|
this.activePrompt = void 0;
|
|
@@ -7778,6 +8303,12 @@ var AcpConnection = class {
|
|
|
7778
8303
|
this.stderr = "";
|
|
7779
8304
|
this.pending.clear();
|
|
7780
8305
|
this.messageQueue = Promise.resolve();
|
|
8306
|
+
child.stdin?.on("error", (error) => {
|
|
8307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8308
|
+
this.stderr += `[ai-connect] ACP stdin error: ${message}
|
|
8309
|
+
`;
|
|
8310
|
+
});
|
|
8311
|
+
trackChild(child, process.platform !== "win32");
|
|
7781
8312
|
child.stdout.setEncoding("utf8");
|
|
7782
8313
|
child.stdout.on("data", (chunk) => {
|
|
7783
8314
|
this.buffer += chunk;
|
|
@@ -7841,8 +8372,14 @@ var AcpConnection = class {
|
|
|
7841
8372
|
if (!stdin) {
|
|
7842
8373
|
return;
|
|
7843
8374
|
}
|
|
7844
|
-
|
|
8375
|
+
try {
|
|
8376
|
+
stdin.write(`${JSON.stringify(message)}
|
|
7845
8377
|
`);
|
|
8378
|
+
} catch (error) {
|
|
8379
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
8380
|
+
this.stderr += `[ai-connect] ACP stdin write error (reply): ${messageText}
|
|
8381
|
+
`;
|
|
8382
|
+
}
|
|
7846
8383
|
}
|
|
7847
8384
|
failPending(error) {
|
|
7848
8385
|
for (const entry of this.pending.values()) {
|
|
@@ -7863,7 +8400,7 @@ var AcpConnection = class {
|
|
|
7863
8400
|
}
|
|
7864
8401
|
if (message.method === "readTextFile") {
|
|
7865
8402
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
7866
|
-
if (!filePath || !isWithinRoot(this.cwd, filePath)) {
|
|
8403
|
+
if (!filePath || !await isWithinRoot(this.cwd, filePath)) {
|
|
7867
8404
|
this.reply({
|
|
7868
8405
|
jsonrpc: "2.0",
|
|
7869
8406
|
id: message.id,
|
|
@@ -7896,7 +8433,7 @@ var AcpConnection = class {
|
|
|
7896
8433
|
}
|
|
7897
8434
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
7898
8435
|
const content = typeof params.content === "string" ? params.content : void 0;
|
|
7899
|
-
if (!filePath || content === void 0 || !isWithinRoot(this.cwd, filePath)) {
|
|
8436
|
+
if (!filePath || content === void 0 || !await isWithinRoot(this.cwd, filePath)) {
|
|
7900
8437
|
this.reply({
|
|
7901
8438
|
jsonrpc: "2.0",
|
|
7902
8439
|
id: message.id,
|
|
@@ -8170,8 +8707,20 @@ var AcpConnection = class {
|
|
|
8170
8707
|
);
|
|
8171
8708
|
return;
|
|
8172
8709
|
}
|
|
8173
|
-
|
|
8710
|
+
try {
|
|
8711
|
+
stdin.write(`${JSON.stringify(payload)}
|
|
8174
8712
|
`);
|
|
8713
|
+
} catch (error) {
|
|
8714
|
+
clearTimeout(timer);
|
|
8715
|
+
this.pending.delete(id);
|
|
8716
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
8717
|
+
reject(
|
|
8718
|
+
new AiConnectError(
|
|
8719
|
+
"local_harness_unavailable",
|
|
8720
|
+
`ACP command "${this.commandLine}" stdin write failed: ${messageText}`
|
|
8721
|
+
)
|
|
8722
|
+
);
|
|
8723
|
+
}
|
|
8175
8724
|
});
|
|
8176
8725
|
}
|
|
8177
8726
|
};
|
|
@@ -8442,6 +8991,10 @@ function createAcpTransportManager(options) {
|
|
|
8442
8991
|
let notify;
|
|
8443
8992
|
const onDelta = (text) => {
|
|
8444
8993
|
deltas.push(text);
|
|
8994
|
+
if (deltas.length > MAX_ACP_STREAM_QUEUE) {
|
|
8995
|
+
const overflow = deltas.splice(0, deltas.length - MAX_ACP_STREAM_QUEUE);
|
|
8996
|
+
deltas.unshift(overflow.join(""));
|
|
8997
|
+
}
|
|
8445
8998
|
notify?.();
|
|
8446
8999
|
};
|
|
8447
9000
|
let settled = false;
|
|
@@ -8579,44 +9132,29 @@ ${message.content}`;
|
|
|
8579
9132
|
}).join("\n\n");
|
|
8580
9133
|
}
|
|
8581
9134
|
function resolveCliCommand(route, options) {
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
}
|
|
8585
|
-
const candidates = [
|
|
8586
|
-
`${route.provider}:${route.transport.id}`,
|
|
8587
|
-
route.transport.id,
|
|
8588
|
-
route.provider
|
|
8589
|
-
];
|
|
8590
|
-
for (const candidate of candidates) {
|
|
8591
|
-
const override = options?.commands?.[candidate]?.trim();
|
|
8592
|
-
if (override) {
|
|
8593
|
-
return override;
|
|
8594
|
-
}
|
|
8595
|
-
}
|
|
9135
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
9136
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
8596
9137
|
const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
9138
|
+
const presetCommand = (presetId ? getCliTransportPreset(presetId).command.trim() : "") || AI_CONNECT_DEFAULT_CLI_COMMANDS[key] || void 0;
|
|
9139
|
+
try {
|
|
9140
|
+
return resolveManagedCommand(
|
|
9141
|
+
key,
|
|
9142
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
9143
|
+
route.transport.command,
|
|
9144
|
+
presetCommand
|
|
9145
|
+
);
|
|
9146
|
+
} catch {
|
|
9147
|
+
throw new AiConnectError(
|
|
9148
|
+
"validation_error",
|
|
9149
|
+
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
9150
|
+
);
|
|
8606
9151
|
}
|
|
8607
|
-
throw new AiConnectError(
|
|
8608
|
-
"validation_error",
|
|
8609
|
-
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
8610
|
-
);
|
|
8611
9152
|
}
|
|
8612
9153
|
function resolveCliExecutable(route, options) {
|
|
8613
9154
|
return splitCommandLine2(resolveCliCommand(route, options)).command;
|
|
8614
9155
|
}
|
|
8615
9156
|
function buildCliEnvironment(options) {
|
|
8616
|
-
return {
|
|
8617
|
-
...process.env,
|
|
8618
|
-
...options?.env ?? {}
|
|
8619
|
-
};
|
|
9157
|
+
return resolveChildEnv({ extra: options?.env ?? {} });
|
|
8620
9158
|
}
|
|
8621
9159
|
function buildCliCwd(context, options) {
|
|
8622
9160
|
return path2.resolve(
|
|
@@ -8717,9 +9255,13 @@ function resolveCliTransportOptions(route) {
|
|
|
8717
9255
|
} : void 0;
|
|
8718
9256
|
const merged = {
|
|
8719
9257
|
...preset?.options ?? {},
|
|
9258
|
+
// `...route.transport.cli` already spreads argsTemplate + parser (when set on
|
|
9259
|
+
// the route) over the preset defaults, so the previously-present conditional
|
|
9260
|
+
// re-spreads of those two fields were dead/redundant (they re-assigned the
|
|
9261
|
+
// identical value) and have been dropped. fileInput/preset below are NOT
|
|
9262
|
+
// redundant: fileInput is a per-field DEEP merge (mergedFileInput) and preset
|
|
9263
|
+
// is derived (route.transport.cli.preset ?? default), not a plain passthrough.
|
|
8720
9264
|
...route.transport.cli ?? {},
|
|
8721
|
-
...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
|
|
8722
|
-
...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
|
|
8723
9265
|
...mergedFileInput ? { fileInput: mergedFileInput } : {},
|
|
8724
9266
|
...presetId ? { preset: presetId } : {}
|
|
8725
9267
|
};
|
|
@@ -8816,7 +9358,7 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
|
|
|
8816
9358
|
}
|
|
8817
9359
|
return values.outputFile;
|
|
8818
9360
|
default:
|
|
8819
|
-
if (templateArg.startsWith("--")) {
|
|
9361
|
+
if (templateArg.startsWith("--") && templateArg.length > 2) {
|
|
8820
9362
|
parameterKeys.push(templateArg);
|
|
8821
9363
|
}
|
|
8822
9364
|
return templateArg;
|
|
@@ -8969,8 +9511,19 @@ function getValueByPath(value, dotPath) {
|
|
|
8969
9511
|
}
|
|
8970
9512
|
return current;
|
|
8971
9513
|
}
|
|
8972
|
-
function
|
|
8973
|
-
|
|
9514
|
+
function parseCliJson(raw, context) {
|
|
9515
|
+
try {
|
|
9516
|
+
return JSON.parse(raw);
|
|
9517
|
+
} catch (error) {
|
|
9518
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
9519
|
+
throw new AiConnectError(
|
|
9520
|
+
"temporary_unavailable",
|
|
9521
|
+
`${context} produced output that is not valid JSON (${detail}).`
|
|
9522
|
+
);
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
function splitJsonlLines(stdout, context = "CLI jsonl parser") {
|
|
9526
|
+
return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => parseCliJson(line, context));
|
|
8974
9527
|
}
|
|
8975
9528
|
function normalizeErrorMessage(value) {
|
|
8976
9529
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -8996,8 +9549,8 @@ function findJsonlSelection(entries, selector) {
|
|
|
8996
9549
|
}
|
|
8997
9550
|
return void 0;
|
|
8998
9551
|
}
|
|
8999
|
-
function parseGenericJsonCli(stdout, parser) {
|
|
9000
|
-
const payload =
|
|
9552
|
+
function parseGenericJsonCli(stdout, parser, context) {
|
|
9553
|
+
const payload = parseCliJson(stdout, context);
|
|
9001
9554
|
const errorValue = parser.errorPath ? getValueByPath(payload, parser.errorPath) : void 0;
|
|
9002
9555
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9003
9556
|
if (errorMessage) {
|
|
@@ -9018,8 +9571,8 @@ function parseGenericJsonCli(stdout, parser) {
|
|
|
9018
9571
|
data: payload
|
|
9019
9572
|
};
|
|
9020
9573
|
}
|
|
9021
|
-
function parseGenericJsonlCli(stdout, parser) {
|
|
9022
|
-
const entries = splitJsonlLines(stdout);
|
|
9574
|
+
function parseGenericJsonlCli(stdout, parser, context) {
|
|
9575
|
+
const entries = splitJsonlLines(stdout, context);
|
|
9023
9576
|
const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
|
|
9024
9577
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9025
9578
|
if (errorMessage) {
|
|
@@ -9093,7 +9646,7 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9093
9646
|
);
|
|
9094
9647
|
}
|
|
9095
9648
|
const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
|
|
9096
|
-
const payload =
|
|
9649
|
+
const payload = parseCliJson(stdout, "CLI discovery json parser");
|
|
9097
9650
|
const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
|
|
9098
9651
|
return Array.isArray(arr) ? arr : [];
|
|
9099
9652
|
})();
|
|
@@ -9106,8 +9659,8 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9106
9659
|
}
|
|
9107
9660
|
return models;
|
|
9108
9661
|
}
|
|
9109
|
-
function parseClaudeCli(stdout) {
|
|
9110
|
-
const payload =
|
|
9662
|
+
function parseClaudeCli(stdout, context) {
|
|
9663
|
+
const payload = parseCliJson(stdout, context);
|
|
9111
9664
|
const text = typeof payload.result === "string" ? payload.result : typeof payload.response === "string" ? payload.response : typeof payload.text === "string" ? payload.text : void 0;
|
|
9112
9665
|
if (!text) {
|
|
9113
9666
|
throw new AiConnectError(
|
|
@@ -9122,8 +9675,8 @@ function parseClaudeCli(stdout) {
|
|
|
9122
9675
|
data: payload
|
|
9123
9676
|
};
|
|
9124
9677
|
}
|
|
9125
|
-
function parseCodexCli(stdout, outputFileContent) {
|
|
9126
|
-
const events = splitJsonlLines(stdout);
|
|
9678
|
+
function parseCodexCli(stdout, outputFileContent, context) {
|
|
9679
|
+
const events = splitJsonlLines(stdout, context);
|
|
9127
9680
|
let text = outputFileContent?.trim() || void 0;
|
|
9128
9681
|
let usage;
|
|
9129
9682
|
for (const event of events) {
|
|
@@ -9173,6 +9726,7 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9173
9726
|
);
|
|
9174
9727
|
}
|
|
9175
9728
|
const cliOptions = resolveCliTransportOptions(route);
|
|
9729
|
+
const parseContext = `CLI transport "${route.transport.id}"${cliOptions.preset ? ` (preset ${cliOptions.preset})` : ""}`;
|
|
9176
9730
|
if (route.transport.cli?.parser) {
|
|
9177
9731
|
const parser = cliOptions.parser;
|
|
9178
9732
|
if (!parser) {
|
|
@@ -9185,9 +9739,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9185
9739
|
case "text":
|
|
9186
9740
|
return parseTextCli(result.stdout, parser);
|
|
9187
9741
|
case "json":
|
|
9188
|
-
return parseGenericJsonCli(stdout, parser);
|
|
9742
|
+
return parseGenericJsonCli(stdout, parser, parseContext);
|
|
9189
9743
|
default:
|
|
9190
|
-
return parseGenericJsonlCli(stdout, parser);
|
|
9744
|
+
return parseGenericJsonlCli(stdout, parser, parseContext);
|
|
9191
9745
|
}
|
|
9192
9746
|
}
|
|
9193
9747
|
switch (cliOptions.preset) {
|
|
@@ -9195,9 +9749,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9195
9749
|
return parseTextCli(stdout, { kind: "text" });
|
|
9196
9750
|
case "claude":
|
|
9197
9751
|
case "openclaude":
|
|
9198
|
-
return parseClaudeCli(stdout);
|
|
9752
|
+
return parseClaudeCli(stdout, parseContext);
|
|
9199
9753
|
case "codex":
|
|
9200
|
-
return parseCodexCli(stdout, outputFileContent);
|
|
9754
|
+
return parseCodexCli(stdout, outputFileContent, parseContext);
|
|
9201
9755
|
default:
|
|
9202
9756
|
throw new AiConnectError(
|
|
9203
9757
|
"not_supported",
|
|
@@ -9211,29 +9765,27 @@ function capturePhase(phases, name, startedAt) {
|
|
|
9211
9765
|
durationMs: Date.now() - startedAt
|
|
9212
9766
|
});
|
|
9213
9767
|
}
|
|
9768
|
+
var MAX_CLI_OUTPUT_CHARS = 64 * 1024 * 1024;
|
|
9214
9769
|
async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
9215
9770
|
const spawnStartedAt = Date.now();
|
|
9771
|
+
const detached = process.platform !== "win32";
|
|
9216
9772
|
const child = spawn2(invocation.command, invocation.args, {
|
|
9217
9773
|
cwd: invocation.cwd,
|
|
9218
9774
|
env: invocation.env,
|
|
9219
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
9775
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9776
|
+
detached
|
|
9220
9777
|
});
|
|
9221
9778
|
capturePhase(phases, "spawn", spawnStartedAt);
|
|
9222
9779
|
let stdout = "";
|
|
9223
9780
|
let stderr = "";
|
|
9224
9781
|
child.stdout.setEncoding("utf8");
|
|
9225
|
-
child.stdout.on("data", (chunk) => {
|
|
9226
|
-
stdout += chunk;
|
|
9227
|
-
});
|
|
9228
9782
|
child.stderr.setEncoding("utf8");
|
|
9229
|
-
child.stderr.on("data", (chunk) => {
|
|
9230
|
-
stderr += chunk;
|
|
9231
|
-
});
|
|
9232
9783
|
const executeStartedAt = Date.now();
|
|
9233
9784
|
return await new Promise((resolve, reject) => {
|
|
9234
9785
|
let abortListener;
|
|
9786
|
+
let overflowed = false;
|
|
9235
9787
|
const timer = setTimeout(() => {
|
|
9236
|
-
child
|
|
9788
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9237
9789
|
reject(
|
|
9238
9790
|
new AiConnectError(
|
|
9239
9791
|
"temporary_unavailable",
|
|
@@ -9248,15 +9800,43 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9248
9800
|
abortListener = void 0;
|
|
9249
9801
|
}
|
|
9250
9802
|
};
|
|
9803
|
+
const enforceBufferCap = () => {
|
|
9804
|
+
if (overflowed || stdout.length + stderr.length <= MAX_CLI_OUTPUT_CHARS) {
|
|
9805
|
+
return;
|
|
9806
|
+
}
|
|
9807
|
+
overflowed = true;
|
|
9808
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9809
|
+
cleanup();
|
|
9810
|
+
reject(
|
|
9811
|
+
new AiConnectError(
|
|
9812
|
+
"temporary_unavailable",
|
|
9813
|
+
`CLI transport "${quoteArg(invocation.command)}" exceeded the ${MAX_CLI_OUTPUT_CHARS}-character output cap.`
|
|
9814
|
+
)
|
|
9815
|
+
);
|
|
9816
|
+
};
|
|
9817
|
+
child.stdout.on("data", (chunk) => {
|
|
9818
|
+
if (overflowed) {
|
|
9819
|
+
return;
|
|
9820
|
+
}
|
|
9821
|
+
stdout += chunk;
|
|
9822
|
+
enforceBufferCap();
|
|
9823
|
+
});
|
|
9824
|
+
child.stderr.on("data", (chunk) => {
|
|
9825
|
+
if (overflowed) {
|
|
9826
|
+
return;
|
|
9827
|
+
}
|
|
9828
|
+
stderr += chunk;
|
|
9829
|
+
enforceBufferCap();
|
|
9830
|
+
});
|
|
9251
9831
|
if (signal) {
|
|
9252
9832
|
if (signal.aborted) {
|
|
9253
|
-
child
|
|
9833
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9254
9834
|
cleanup();
|
|
9255
9835
|
reject(new AiConnectError("aborted", "CLI transport aborted before execution."));
|
|
9256
9836
|
return;
|
|
9257
9837
|
}
|
|
9258
9838
|
abortListener = () => {
|
|
9259
|
-
child
|
|
9839
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9260
9840
|
cleanup();
|
|
9261
9841
|
reject(new AiConnectError("aborted", "CLI transport aborted."));
|
|
9262
9842
|
};
|
|
@@ -9272,6 +9852,9 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9272
9852
|
);
|
|
9273
9853
|
});
|
|
9274
9854
|
child.once("close", (exitCode) => {
|
|
9855
|
+
if (overflowed) {
|
|
9856
|
+
return;
|
|
9857
|
+
}
|
|
9275
9858
|
cleanup();
|
|
9276
9859
|
capturePhase(phases, "execute", executeStartedAt);
|
|
9277
9860
|
resolve({
|
|
@@ -9375,7 +9958,10 @@ function createCliTransportManager(options) {
|
|
|
9375
9958
|
)
|
|
9376
9959
|
);
|
|
9377
9960
|
} finally {
|
|
9378
|
-
|
|
9961
|
+
try {
|
|
9962
|
+
await cleanupCliInvocation(invocation);
|
|
9963
|
+
} catch {
|
|
9964
|
+
}
|
|
9379
9965
|
}
|
|
9380
9966
|
},
|
|
9381
9967
|
async runPrompt(context) {
|
|
@@ -9415,7 +10001,10 @@ function createCliTransportManager(options) {
|
|
|
9415
10001
|
}
|
|
9416
10002
|
return result;
|
|
9417
10003
|
} finally {
|
|
9418
|
-
|
|
10004
|
+
try {
|
|
10005
|
+
await cleanupCliInvocation(invocation);
|
|
10006
|
+
} catch {
|
|
10007
|
+
}
|
|
9419
10008
|
}
|
|
9420
10009
|
}
|
|
9421
10010
|
};
|
|
@@ -9434,9 +10023,12 @@ var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
|
|
|
9434
10023
|
transportId: "opencode-server"
|
|
9435
10024
|
}
|
|
9436
10025
|
};
|
|
9437
|
-
var AI_CONNECT_DEFAULT_SERVER_COMMANDS =
|
|
9438
|
-
|
|
9439
|
-
}
|
|
10026
|
+
var AI_CONNECT_DEFAULT_SERVER_COMMANDS = Object.fromEntries(
|
|
10027
|
+
Object.values(AI_CONNECT_DEFAULT_SERVER_PRESETS).map((preset) => [
|
|
10028
|
+
`${preset.id}:${preset.transportId}`,
|
|
10029
|
+
preset.command
|
|
10030
|
+
])
|
|
10031
|
+
);
|
|
9440
10032
|
|
|
9441
10033
|
// src/server.ts
|
|
9442
10034
|
function splitCommandLine3(commandLine) {
|
|
@@ -9457,28 +10049,21 @@ function splitCommandLine3(commandLine) {
|
|
|
9457
10049
|
};
|
|
9458
10050
|
}
|
|
9459
10051
|
function resolveServerCommand(route, options) {
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
const preset = AI_CONNECT_DEFAULT_SERVER_COMMANDS[`${route.provider}:${route.transport.id}`];
|
|
9475
|
-
if (preset) {
|
|
9476
|
-
return preset;
|
|
10052
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
10053
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
10054
|
+
try {
|
|
10055
|
+
return resolveManagedCommand(
|
|
10056
|
+
key,
|
|
10057
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
10058
|
+
route.transport.command,
|
|
10059
|
+
AI_CONNECT_DEFAULT_SERVER_COMMANDS[key]
|
|
10060
|
+
);
|
|
10061
|
+
} catch {
|
|
10062
|
+
throw new AiConnectError(
|
|
10063
|
+
"validation_error",
|
|
10064
|
+
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
10065
|
+
);
|
|
9477
10066
|
}
|
|
9478
|
-
throw new AiConnectError(
|
|
9479
|
-
"validation_error",
|
|
9480
|
-
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
9481
|
-
);
|
|
9482
10067
|
}
|
|
9483
10068
|
function resolveServerExecutable(route, options) {
|
|
9484
10069
|
return splitCommandLine3(resolveServerCommand(route, options)).command;
|
|
@@ -9550,9 +10135,12 @@ async function findAvailablePort(host = "127.0.0.1") {
|
|
|
9550
10135
|
async function waitForServerHealth(fetchLike, origin, timeoutMs) {
|
|
9551
10136
|
const deadline = Date.now() + timeoutMs;
|
|
9552
10137
|
let lastError;
|
|
10138
|
+
const perAttemptTimeoutMs = Math.min(2e3, Math.max(250, timeoutMs));
|
|
9553
10139
|
while (Date.now() < deadline) {
|
|
9554
10140
|
try {
|
|
9555
|
-
const response = await fetchLike(joinUrl(origin, "/global/health")
|
|
10141
|
+
const response = await fetchLike(joinUrl(origin, "/global/health"), {
|
|
10142
|
+
signal: AbortSignal.timeout(perAttemptTimeoutMs)
|
|
10143
|
+
});
|
|
9556
10144
|
if (response.ok) {
|
|
9557
10145
|
return;
|
|
9558
10146
|
}
|
|
@@ -9582,14 +10170,14 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9582
10170
|
}
|
|
9583
10171
|
args.push("--hostname", host, "--port", String(port));
|
|
9584
10172
|
const spawnStartedAt = Date.now();
|
|
10173
|
+
const detached = process.platform !== "win32";
|
|
9585
10174
|
const child = spawn3(resolved.command, args, {
|
|
9586
10175
|
cwd: cwdOverride ?? options?.cwd ?? process.cwd(),
|
|
9587
|
-
env: {
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
},
|
|
9591
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10176
|
+
env: resolveChildEnv({ extra: options?.env ?? {} }),
|
|
10177
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10178
|
+
detached
|
|
9592
10179
|
});
|
|
10180
|
+
const untrack = trackChild(child, detached);
|
|
9593
10181
|
let stderr = "";
|
|
9594
10182
|
child.stderr.setEncoding("utf8");
|
|
9595
10183
|
child.stderr.on("data", (chunk) => {
|
|
@@ -9605,7 +10193,8 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9605
10193
|
);
|
|
9606
10194
|
capturePhase2(phases, "health", healthStartedAt);
|
|
9607
10195
|
} catch (error) {
|
|
9608
|
-
|
|
10196
|
+
untrack();
|
|
10197
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9609
10198
|
throw new AiConnectError(
|
|
9610
10199
|
"local_harness_unavailable",
|
|
9611
10200
|
stderr.trim() || (error instanceof Error ? error.message : "Failed to start local server transport.")
|
|
@@ -9613,15 +10202,17 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9613
10202
|
}
|
|
9614
10203
|
return {
|
|
9615
10204
|
origin,
|
|
10205
|
+
child,
|
|
9616
10206
|
dispose: async () => {
|
|
10207
|
+
untrack();
|
|
9617
10208
|
if (child.killed) {
|
|
9618
10209
|
return;
|
|
9619
10210
|
}
|
|
9620
|
-
child
|
|
10211
|
+
killProcessTree(child, "SIGTERM", detached);
|
|
9621
10212
|
await new Promise((resolve) => {
|
|
9622
10213
|
child.once("close", () => resolve());
|
|
9623
10214
|
setTimeout(() => {
|
|
9624
|
-
child
|
|
10215
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9625
10216
|
resolve();
|
|
9626
10217
|
}, 1e3).unref?.();
|
|
9627
10218
|
});
|
|
@@ -9721,7 +10312,17 @@ function createServerTransportManager(options) {
|
|
|
9721
10312
|
const pending = spawnServerInstance(route, options, phases, cwdOverride);
|
|
9722
10313
|
instances.set(key, pending);
|
|
9723
10314
|
try {
|
|
9724
|
-
|
|
10315
|
+
const instance = await pending;
|
|
10316
|
+
if (instance.child) {
|
|
10317
|
+
const evict = () => {
|
|
10318
|
+
if (instances.get(key) === pending) {
|
|
10319
|
+
instances.delete(key);
|
|
10320
|
+
}
|
|
10321
|
+
};
|
|
10322
|
+
instance.child.once("exit", evict);
|
|
10323
|
+
instance.child.once("error", evict);
|
|
10324
|
+
}
|
|
10325
|
+
return instance;
|
|
9725
10326
|
} catch (error) {
|
|
9726
10327
|
instances.delete(key);
|
|
9727
10328
|
throw error;
|
|
@@ -9751,7 +10352,10 @@ function createServerTransportManager(options) {
|
|
|
9751
10352
|
headers: {
|
|
9752
10353
|
"content-type": "application/json"
|
|
9753
10354
|
},
|
|
9754
|
-
body: JSON.stringify({})
|
|
10355
|
+
body: JSON.stringify({}),
|
|
10356
|
+
// H6: honor caller cancel / operation timeout on session creation too
|
|
10357
|
+
// (was only wired on the message fetch).
|
|
10358
|
+
signal: context.abort.signal
|
|
9755
10359
|
});
|
|
9756
10360
|
capturePhase2(phases, "session/create", sessionStartedAt);
|
|
9757
10361
|
if (!sessionResponse.ok) {
|
|
@@ -9820,7 +10424,10 @@ function createServerTransportManager(options) {
|
|
|
9820
10424
|
};
|
|
9821
10425
|
context.telemetry?.captureTransport(transport);
|
|
9822
10426
|
const startedAt = Date.now();
|
|
9823
|
-
const response = await fetchLike(
|
|
10427
|
+
const response = await fetchLike(
|
|
10428
|
+
joinUrl(instance.origin, "/config/providers"),
|
|
10429
|
+
{ signal: context.abort.signal }
|
|
10430
|
+
);
|
|
9824
10431
|
capturePhase2(phases, "config/providers", startedAt);
|
|
9825
10432
|
if (!response.ok) {
|
|
9826
10433
|
throw new AiConnectError(
|
|
@@ -9864,6 +10471,15 @@ function createServerTransportManager(options) {
|
|
|
9864
10471
|
}
|
|
9865
10472
|
|
|
9866
10473
|
// src/local-handlers.ts
|
|
10474
|
+
function normalizeVerifyIssue(error, fallbackCode, fallbackMessage) {
|
|
10475
|
+
const normalized = error instanceof AiConnectError ? error : new AiConnectError(fallbackCode, fallbackMessage);
|
|
10476
|
+
return [
|
|
10477
|
+
{
|
|
10478
|
+
code: "handler_issue",
|
|
10479
|
+
message: normalized.message
|
|
10480
|
+
}
|
|
10481
|
+
];
|
|
10482
|
+
}
|
|
9867
10483
|
function createLocalRouteHandlers(options = {}) {
|
|
9868
10484
|
const acpTransport = createAcpTransportManager(options.acp);
|
|
9869
10485
|
const cliTransport = createCliTransportManager(options.cli);
|
|
@@ -9894,16 +10510,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
9894
10510
|
}
|
|
9895
10511
|
];
|
|
9896
10512
|
} catch (error) {
|
|
9897
|
-
|
|
10513
|
+
return normalizeVerifyIssue(
|
|
10514
|
+
error,
|
|
9898
10515
|
"validation_error",
|
|
9899
10516
|
"ACP route verification failed."
|
|
9900
10517
|
);
|
|
9901
|
-
return [
|
|
9902
|
-
{
|
|
9903
|
-
code: "handler_issue",
|
|
9904
|
-
message: normalized.message
|
|
9905
|
-
}
|
|
9906
|
-
];
|
|
9907
10518
|
}
|
|
9908
10519
|
}
|
|
9909
10520
|
},
|
|
@@ -9942,16 +10553,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
9942
10553
|
}
|
|
9943
10554
|
];
|
|
9944
10555
|
} catch (error) {
|
|
9945
|
-
|
|
10556
|
+
return normalizeVerifyIssue(
|
|
10557
|
+
error,
|
|
9946
10558
|
"validation_error",
|
|
9947
10559
|
"CLI route verification failed."
|
|
9948
10560
|
);
|
|
9949
|
-
return [
|
|
9950
|
-
{
|
|
9951
|
-
code: "handler_issue",
|
|
9952
|
-
message: normalized.message
|
|
9953
|
-
}
|
|
9954
|
-
];
|
|
9955
10561
|
}
|
|
9956
10562
|
}
|
|
9957
10563
|
},
|
|
@@ -9987,16 +10593,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
9987
10593
|
}
|
|
9988
10594
|
];
|
|
9989
10595
|
} catch (error) {
|
|
9990
|
-
|
|
10596
|
+
return normalizeVerifyIssue(
|
|
10597
|
+
error,
|
|
9991
10598
|
"validation_error",
|
|
9992
10599
|
"Server route verification failed."
|
|
9993
10600
|
);
|
|
9994
|
-
return [
|
|
9995
|
-
{
|
|
9996
|
-
code: "handler_issue",
|
|
9997
|
-
message: normalized.message
|
|
9998
|
-
}
|
|
9999
|
-
];
|
|
10000
10601
|
}
|
|
10001
10602
|
}
|
|
10002
10603
|
}
|