@vedmalex/ai-connect 0.7.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 +37 -4
- package/dist/browser/index.js +717 -284
- package/dist/browser/index.js.map +3 -3
- package/dist/bun/index.js +1248 -450
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/local.js +1194 -444
- package/dist/bun/local.js.map +4 -4
- package/dist/node/index.js +1248 -450
- package/dist/node/index.js.map +4 -4
- package/dist/node/local.js +1194 -444
- package/dist/node/local.js.map +4 -4
- package/dist/types/acp.d.ts +14 -0
- 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 +44 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bun/index.js
CHANGED
|
@@ -12,7 +12,10 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
12
12
|
// pi takes file/image attachments as separate `@<path>` argv tokens (REQ-024);
|
|
13
13
|
// {files} expands one `@<staged-path>` token per attachment. pi stages all
|
|
14
14
|
// categories (PDFs as-is, no extraction guarantee).
|
|
15
|
-
|
|
15
|
+
// H10 (flag injection): the `--` end-of-options token precedes {files}/{prompt}
|
|
16
|
+
// — both are BARE positionals here — so an `@<path>` reference or a prompt that
|
|
17
|
+
// starts with `-` is parsed by pi as a positional, never as a flag.
|
|
18
|
+
argsTemplate: ["--print", "--model", "{model}", "--", "{files}", "{prompt}"],
|
|
16
19
|
discovery: {
|
|
17
20
|
via: "none"
|
|
18
21
|
},
|
|
@@ -32,13 +35,22 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
32
35
|
transportId: "claude-cli",
|
|
33
36
|
options: {
|
|
34
37
|
preset: "claude",
|
|
35
|
-
|
|
38
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional so a
|
|
39
|
+
// prompt starting with `-` (e.g. "--dangerously-skip-permissions") is parsed
|
|
40
|
+
// by the claude CLI as a positional, not a flag.
|
|
41
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
36
42
|
discovery: {
|
|
37
43
|
via: "acp",
|
|
38
44
|
acp: {
|
|
39
45
|
transportId: "claude-code-acp"
|
|
40
46
|
}
|
|
41
47
|
},
|
|
48
|
+
// SENTINEL (documentation-only): the `__preset__:*` textPath/errorPath values
|
|
49
|
+
// are NOT resolved as live JSON paths. This `parser` block records which
|
|
50
|
+
// dedicated parser a built-in preset maps to (parseClaudeCli); dispatch for a
|
|
51
|
+
// preset route goes through the `cliOptions.preset` switch in cli.ts
|
|
52
|
+
// (parseCliResult), which only reads a parser config when a ROUTE sets its own
|
|
53
|
+
// `cli.parser`. See the mirrored note in cli.ts::parseCliResult.
|
|
42
54
|
parser: {
|
|
43
55
|
kind: "json",
|
|
44
56
|
textPath: "__preset__:claude.result",
|
|
@@ -54,7 +66,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
54
66
|
transportId: "openclaude-cli",
|
|
55
67
|
options: {
|
|
56
68
|
preset: "openclaude",
|
|
57
|
-
|
|
69
|
+
// H10 (flag injection): `--` precedes the trailing {prompt} positional (see
|
|
70
|
+
// the claude preset above for rationale).
|
|
71
|
+
argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "--", "{prompt}"],
|
|
72
|
+
// SENTINEL (documentation-only): see the claude preset note — `__preset__:*`
|
|
73
|
+
// paths are markers, not live JSON paths; dispatch is via the preset switch.
|
|
58
74
|
parser: {
|
|
59
75
|
kind: "json",
|
|
60
76
|
textPath: "__preset__:claude.result",
|
|
@@ -73,6 +89,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
73
89
|
// codex exec takes image attachments via `-i/--image <FILE>` flags
|
|
74
90
|
// (REQ-024); {files} expands a `--image <staged-path>` pair per image.
|
|
75
91
|
// Images only — a non-image attachment cleanly rejects pre-spawn.
|
|
92
|
+
// H10 (flag injection): `--` sits AFTER {files} and before the trailing
|
|
93
|
+
// {prompt} positional. Unlike pi, codex files are passed via the explicit
|
|
94
|
+
// `--image <path>` flag (the path is that flag's VALUE, not a positional), so
|
|
95
|
+
// `--` must NOT precede {files} or it would neutralize `--image`. It guards
|
|
96
|
+
// only the {prompt} positional against being parsed as a flag.
|
|
76
97
|
argsTemplate: [
|
|
77
98
|
"exec",
|
|
78
99
|
"--json",
|
|
@@ -81,6 +102,7 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
81
102
|
"--model",
|
|
82
103
|
"{model}",
|
|
83
104
|
"{files}",
|
|
105
|
+
"--",
|
|
84
106
|
"{prompt}"
|
|
85
107
|
],
|
|
86
108
|
discovery: {
|
|
@@ -94,6 +116,11 @@ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
|
|
|
94
116
|
perFileArgs: ["--image", "{path}"],
|
|
95
117
|
categories: ["image"]
|
|
96
118
|
},
|
|
119
|
+
// SENTINEL (documentation-only): the `__preset__:*` path values below are
|
|
120
|
+
// markers naming the dedicated parser this preset maps to (parseCodexCli), NOT
|
|
121
|
+
// live JSONL selectors. Dispatch for a preset route runs through the
|
|
122
|
+
// `cliOptions.preset` switch in cli.ts::parseCliResult; this block is only ever
|
|
123
|
+
// read as a live parser when a ROUTE overrides `cli.parser`.
|
|
97
124
|
parser: {
|
|
98
125
|
kind: "jsonl",
|
|
99
126
|
text: {
|
|
@@ -130,6 +157,19 @@ var AiConnectError = class extends Error {
|
|
|
130
157
|
this.code = code;
|
|
131
158
|
this.details = details;
|
|
132
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* `Error.prototype.message` is non-enumerable, so a bare `JSON.stringify(err)` silently drops
|
|
162
|
+
* it (LOW, review-report-seed.md). Provide an explicit projection so serializing an
|
|
163
|
+
* `AiConnectError` (logs, wide events, error-response bodies) always carries the message.
|
|
164
|
+
*/
|
|
165
|
+
toJSON() {
|
|
166
|
+
return {
|
|
167
|
+
name: this.name,
|
|
168
|
+
code: this.code,
|
|
169
|
+
message: this.message,
|
|
170
|
+
details: this.details
|
|
171
|
+
};
|
|
172
|
+
}
|
|
133
173
|
};
|
|
134
174
|
function isAiConnectError(error) {
|
|
135
175
|
return error instanceof AiConnectError;
|
|
@@ -160,6 +200,16 @@ function toAiConnectError(error, fallbackCode = "temporary_unavailable") {
|
|
|
160
200
|
// src/config.ts
|
|
161
201
|
var DEFAULT_OPERATIONS = ["text", "image", "file"];
|
|
162
202
|
var DEFAULT_STRATEGY = "priority";
|
|
203
|
+
var CLI_TRANSPORT_ID_BY_PROVIDER = {
|
|
204
|
+
anthropic: "claude-cli",
|
|
205
|
+
openclaude: "openclaude-cli",
|
|
206
|
+
openai: "codex-cli",
|
|
207
|
+
pi: "pi-cli"
|
|
208
|
+
};
|
|
209
|
+
var ACP_TRANSPORT_ID_BY_PROVIDER = {
|
|
210
|
+
anthropic: "claude-code-acp",
|
|
211
|
+
openai: "codex-acp"
|
|
212
|
+
};
|
|
163
213
|
var DEFAULT_FALLBACK_ACTIONS = {
|
|
164
214
|
rate_limit: [
|
|
165
215
|
"rotate-credential",
|
|
@@ -271,33 +321,64 @@ function assert(condition, message) {
|
|
|
271
321
|
throw new AiConnectError("validation_error", message);
|
|
272
322
|
}
|
|
273
323
|
}
|
|
274
|
-
function asPositiveInteger(value, fallback) {
|
|
324
|
+
function asPositiveInteger(value, fallback, fieldLabel) {
|
|
325
|
+
if (value === void 0) {
|
|
326
|
+
return fallback;
|
|
327
|
+
}
|
|
275
328
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
329
|
+
if (fieldLabel) {
|
|
330
|
+
console.warn(
|
|
331
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
276
334
|
return fallback;
|
|
277
335
|
}
|
|
278
336
|
return Math.floor(value);
|
|
279
337
|
}
|
|
280
|
-
function asNonNegativeInteger(value, fallback) {
|
|
338
|
+
function asNonNegativeInteger(value, fallback, fieldLabel) {
|
|
339
|
+
if (value === void 0) {
|
|
340
|
+
return fallback;
|
|
341
|
+
}
|
|
281
342
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
343
|
+
if (fieldLabel) {
|
|
344
|
+
console.warn(
|
|
345
|
+
`[ai-connect] ${fieldLabel} must be a non-negative finite number when provided; received ${JSON.stringify(value)}. Falling back to ${fallback}.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
282
348
|
return fallback;
|
|
283
349
|
}
|
|
284
350
|
return Math.floor(value);
|
|
285
351
|
}
|
|
286
|
-
function asOptionalContextWindow(value) {
|
|
352
|
+
function asOptionalContextWindow(value, fieldLabel) {
|
|
353
|
+
if (value === void 0) {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
287
356
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
357
|
+
if (fieldLabel) {
|
|
358
|
+
console.warn(
|
|
359
|
+
`[ai-connect] ${fieldLabel} must be a positive finite number when provided; received ${JSON.stringify(value)}. Ignoring.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
288
362
|
return void 0;
|
|
289
363
|
}
|
|
290
364
|
return Math.floor(value);
|
|
291
365
|
}
|
|
292
|
-
function normalizeModelInput(input, accountContextWindow) {
|
|
366
|
+
function normalizeModelInput(input, accountContextWindow, accountId, providerId) {
|
|
293
367
|
if (typeof input === "string") {
|
|
294
368
|
return {
|
|
295
369
|
id: input.trim(),
|
|
296
370
|
...accountContextWindow !== void 0 ? { contextWindow: accountContextWindow } : {}
|
|
297
371
|
};
|
|
298
372
|
}
|
|
373
|
+
assert(
|
|
374
|
+
typeof input.id === "string" && input.id.trim().length > 0,
|
|
375
|
+
`Account "${accountId}" for provider "${providerId}" declared a model entry with a missing or empty "id".`
|
|
376
|
+
);
|
|
299
377
|
const id = input.id.trim();
|
|
300
|
-
const override = asOptionalContextWindow(
|
|
378
|
+
const override = asOptionalContextWindow(
|
|
379
|
+
input.contextWindow,
|
|
380
|
+
`Model "${id}" (account "${accountId}", provider "${providerId}") contextWindow`
|
|
381
|
+
);
|
|
301
382
|
const contextWindow = override ?? accountContextWindow;
|
|
302
383
|
return {
|
|
303
384
|
id,
|
|
@@ -308,7 +389,8 @@ function asFanoutBound(value) {
|
|
|
308
389
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
309
390
|
return Number.POSITIVE_INFINITY;
|
|
310
391
|
}
|
|
311
|
-
|
|
392
|
+
const floored = Math.floor(value);
|
|
393
|
+
return floored <= 0 ? Number.POSITIVE_INFINITY : floored;
|
|
312
394
|
}
|
|
313
395
|
function normalizeFanoutPolicy(policy) {
|
|
314
396
|
return {
|
|
@@ -534,7 +616,7 @@ function normalizeTransport(providerId, input) {
|
|
|
534
616
|
};
|
|
535
617
|
}
|
|
536
618
|
if (descriptor.kind === "cli") {
|
|
537
|
-
const inferredId2 = providerId
|
|
619
|
+
const inferredId2 = CLI_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "cli";
|
|
538
620
|
return {
|
|
539
621
|
kind: "cli",
|
|
540
622
|
id: descriptor.id?.trim() || inferredId2,
|
|
@@ -551,7 +633,7 @@ function normalizeTransport(providerId, input) {
|
|
|
551
633
|
...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
|
|
552
634
|
};
|
|
553
635
|
}
|
|
554
|
-
const inferredId = providerId
|
|
636
|
+
const inferredId = ACP_TRANSPORT_ID_BY_PROVIDER[providerId] ?? "acp";
|
|
555
637
|
const normalizedLaunch = descriptor.launch ? (() => {
|
|
556
638
|
const contextMode = descriptor.launch?.contextMode ?? "workspace";
|
|
557
639
|
const skillsMode = descriptor.launch?.skillsMode ?? "default";
|
|
@@ -648,20 +730,26 @@ function defaultCapabilities(transport, runtime) {
|
|
|
648
730
|
}
|
|
649
731
|
function normalizeCredential(providerId, accountId, transport, input, index) {
|
|
650
732
|
const id = input.id?.trim() || `${accountId}-credential-${index + 1}`;
|
|
651
|
-
const
|
|
733
|
+
const trimmedApiKey = input.apiKey?.trim();
|
|
734
|
+
const trimmedApiKeyEnv = input.apiKeyEnv?.trim();
|
|
735
|
+
const source = input.source?.trim() || (trimmedApiKey ? "literal" : trimmedApiKeyEnv ? "env" : transport.kind === "api" ? "env" : "session");
|
|
652
736
|
if (transport.kind === "api") {
|
|
653
737
|
assert(
|
|
654
|
-
Boolean(
|
|
738
|
+
Boolean(trimmedApiKey) || Boolean(trimmedApiKeyEnv),
|
|
655
739
|
`API account "${accountId}" for provider "${providerId}" requires apiKey or apiKeyEnv credentials.`
|
|
656
740
|
);
|
|
657
741
|
}
|
|
658
742
|
return {
|
|
659
743
|
id,
|
|
660
744
|
source,
|
|
661
|
-
weight: asPositiveInteger(
|
|
745
|
+
weight: asPositiveInteger(
|
|
746
|
+
input.weight,
|
|
747
|
+
1,
|
|
748
|
+
`Credential "${id}" (account "${accountId}", provider "${providerId}") weight`
|
|
749
|
+
),
|
|
662
750
|
metadata: input.metadata ?? {},
|
|
663
|
-
...
|
|
664
|
-
...
|
|
751
|
+
...trimmedApiKey ? { apiKey: trimmedApiKey } : {},
|
|
752
|
+
...trimmedApiKeyEnv ? { apiKeyEnv: trimmedApiKeyEnv } : {},
|
|
665
753
|
...input.apiKeyDelimiter ? { apiKeyDelimiter: input.apiKeyDelimiter } : {}
|
|
666
754
|
};
|
|
667
755
|
}
|
|
@@ -695,6 +783,31 @@ function buildRouteId(route) {
|
|
|
695
783
|
slugify(route.model)
|
|
696
784
|
].join(":");
|
|
697
785
|
}
|
|
786
|
+
function assertRouteIdUniqueness(routes) {
|
|
787
|
+
const byId = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const route of routes) {
|
|
789
|
+
const group = byId.get(route.id);
|
|
790
|
+
if (group) {
|
|
791
|
+
group.push(route);
|
|
792
|
+
} else {
|
|
793
|
+
byId.set(route.id, [route]);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [id, group] of byId) {
|
|
797
|
+
if (group.length <= 1) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const distinctModels = [...new Set(group.map((route) => route.model))];
|
|
801
|
+
if (distinctModels.length <= 1) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const first = group[0];
|
|
805
|
+
assert(
|
|
806
|
+
false,
|
|
807
|
+
`Route id "${id}" is ambiguous: model(s) ${distinctModels.map((model) => `"${model}"`).join(", ")} on account "${first.accountId}" (provider "${first.provider}") slugify to the same route id. Rename one of the models so they no longer collide after slugification.`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
698
811
|
function routeAliases(route) {
|
|
699
812
|
return [
|
|
700
813
|
route.provider,
|
|
@@ -819,7 +932,8 @@ function defineConfig(input) {
|
|
|
819
932
|
const transport = normalizeTransport(providerId, accountInput.transport);
|
|
820
933
|
const runtime = normalizeRuntime(transport, accountInput.runtime);
|
|
821
934
|
const accountContextWindow = asOptionalContextWindow(
|
|
822
|
-
accountInput.contextWindow
|
|
935
|
+
accountInput.contextWindow,
|
|
936
|
+
`Account "${accountId}" (provider "${providerId}") contextWindow`
|
|
823
937
|
);
|
|
824
938
|
const accountModelAllowlistMode = accountInput.modelAllowlistMode ?? "strict";
|
|
825
939
|
const accountContextMode = accountInput.contextMode ?? "workspace";
|
|
@@ -829,7 +943,9 @@ function defineConfig(input) {
|
|
|
829
943
|
Array.isArray(accountInput.models) && accountInput.models.length > 0,
|
|
830
944
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
831
945
|
);
|
|
832
|
-
const models = accountInput.models.map(
|
|
946
|
+
const models = accountInput.models.map(
|
|
947
|
+
(model) => normalizeModelInput(model, accountContextWindow, accountId, providerId)
|
|
948
|
+
).filter((model) => model.id.length > 0);
|
|
833
949
|
assert(
|
|
834
950
|
models.length > 0,
|
|
835
951
|
`Account "${accountId}" for provider "${providerId}" must declare at least one model.`
|
|
@@ -859,8 +975,16 @@ function defineConfig(input) {
|
|
|
859
975
|
credentials,
|
|
860
976
|
capabilities,
|
|
861
977
|
verify,
|
|
862
|
-
weight: asPositiveInteger(
|
|
863
|
-
|
|
978
|
+
weight: asPositiveInteger(
|
|
979
|
+
accountInput.weight,
|
|
980
|
+
1,
|
|
981
|
+
`Account "${accountId}" (provider "${providerId}") weight`
|
|
982
|
+
),
|
|
983
|
+
priority: asNonNegativeInteger(
|
|
984
|
+
accountInput.priority,
|
|
985
|
+
accountIndex,
|
|
986
|
+
`Account "${accountId}" (provider "${providerId}") priority`
|
|
987
|
+
),
|
|
864
988
|
enabled: accountInput.enabled ?? true
|
|
865
989
|
};
|
|
866
990
|
accounts.push(normalizedAccount);
|
|
@@ -907,6 +1031,7 @@ function defineConfig(input) {
|
|
|
907
1031
|
};
|
|
908
1032
|
}
|
|
909
1033
|
assert(routes.length > 0, "At least one enabled route must be produced.");
|
|
1034
|
+
assertRouteIdUniqueness(routes);
|
|
910
1035
|
finalizeRotationCapabilities(routes);
|
|
911
1036
|
const strategy = input.routing?.strategy ?? DEFAULT_STRATEGY;
|
|
912
1037
|
const resolutionInput = input.routing?.resolution;
|
|
@@ -980,6 +1105,10 @@ function resolveRouteSelectors(selectors, routes, policy) {
|
|
|
980
1105
|
}
|
|
981
1106
|
|
|
982
1107
|
// src/fanout.ts
|
|
1108
|
+
function abortedError() {
|
|
1109
|
+
return new AiConnectError("aborted", "Operation aborted.");
|
|
1110
|
+
}
|
|
1111
|
+
var MAX_DELAY_MS = 24 * 60 * 60 * 1e3;
|
|
983
1112
|
function createFanoutLimiter(policy, runtime) {
|
|
984
1113
|
const now = () => runtime.now?.() ?? Date.now();
|
|
985
1114
|
const { maxConcurrency, requestsPerSecond, maxCalls } = policy;
|
|
@@ -1043,10 +1172,10 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1043
1172
|
if (index >= 0) {
|
|
1044
1173
|
concurrencyQueue.splice(index, 1);
|
|
1045
1174
|
}
|
|
1046
|
-
rejectWaiter(waiter,
|
|
1175
|
+
rejectWaiter(waiter, abortedError());
|
|
1047
1176
|
};
|
|
1048
1177
|
if (signal.aborted) {
|
|
1049
|
-
reject(
|
|
1178
|
+
reject(abortedError());
|
|
1050
1179
|
return;
|
|
1051
1180
|
}
|
|
1052
1181
|
waiter.signal = signal;
|
|
@@ -1062,7 +1191,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1062
1191
|
}
|
|
1063
1192
|
while (true) {
|
|
1064
1193
|
if (signal?.aborted) {
|
|
1065
|
-
throw
|
|
1194
|
+
throw abortedError();
|
|
1066
1195
|
}
|
|
1067
1196
|
refill();
|
|
1068
1197
|
if (tokens >= 1) {
|
|
@@ -1074,6 +1203,7 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1074
1203
|
}
|
|
1075
1204
|
}
|
|
1076
1205
|
function delay(ms, signal) {
|
|
1206
|
+
const safeMs = Number.isFinite(ms) && ms >= 0 ? Math.min(ms, MAX_DELAY_MS) : MAX_DELAY_MS;
|
|
1077
1207
|
return new Promise((resolve, reject) => {
|
|
1078
1208
|
let cancelTimer;
|
|
1079
1209
|
const onAbort = () => {
|
|
@@ -1081,11 +1211,11 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1081
1211
|
if (signal) {
|
|
1082
1212
|
signal.removeEventListener("abort", onAbort);
|
|
1083
1213
|
}
|
|
1084
|
-
reject(
|
|
1214
|
+
reject(abortedError());
|
|
1085
1215
|
};
|
|
1086
1216
|
if (signal) {
|
|
1087
1217
|
if (signal.aborted) {
|
|
1088
|
-
reject(
|
|
1218
|
+
reject(abortedError());
|
|
1089
1219
|
return;
|
|
1090
1220
|
}
|
|
1091
1221
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1097,9 +1227,9 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1097
1227
|
resolve();
|
|
1098
1228
|
};
|
|
1099
1229
|
if (runtime.setTimer) {
|
|
1100
|
-
cancelTimer = runtime.setTimer(
|
|
1230
|
+
cancelTimer = runtime.setTimer(safeMs, done);
|
|
1101
1231
|
} else {
|
|
1102
|
-
const handle = setTimeout(done,
|
|
1232
|
+
const handle = setTimeout(done, safeMs);
|
|
1103
1233
|
cancelTimer = () => clearTimeout(handle);
|
|
1104
1234
|
}
|
|
1105
1235
|
});
|
|
@@ -1113,8 +1243,13 @@ function createFanoutLimiter(policy, runtime) {
|
|
|
1113
1243
|
);
|
|
1114
1244
|
}
|
|
1115
1245
|
callsStarted += 1;
|
|
1116
|
-
|
|
1117
|
-
|
|
1246
|
+
try {
|
|
1247
|
+
await awaitRateToken(signal);
|
|
1248
|
+
await awaitConcurrencySlot(signal);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
callsStarted -= 1;
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1118
1253
|
let released = false;
|
|
1119
1254
|
return () => {
|
|
1120
1255
|
if (released) {
|
|
@@ -1202,19 +1337,54 @@ function isRemoteUrl(value) {
|
|
|
1202
1337
|
return false;
|
|
1203
1338
|
}
|
|
1204
1339
|
}
|
|
1340
|
+
function mimeTypeAndExtensionFromMediaType(mediaTypeAndParams, fallbackMimeType, fallbackExtension) {
|
|
1341
|
+
const mimeType = mediaTypeAndParams.split(";")[0]?.trim() || fallbackMimeType;
|
|
1342
|
+
const extension2 = mimeType.split("/")[1] ?? fallbackExtension;
|
|
1343
|
+
return { mimeType, extension: extension2 };
|
|
1344
|
+
}
|
|
1205
1345
|
function parseDataUrl(value) {
|
|
1206
|
-
const
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1346
|
+
const base64Match = /^data:([^,]*?);base64,(.*)$/i.exec(value);
|
|
1347
|
+
if (base64Match) {
|
|
1348
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1349
|
+
base64Match[1] ?? "",
|
|
1350
|
+
"application/octet-stream",
|
|
1351
|
+
"bin"
|
|
1352
|
+
);
|
|
1353
|
+
return {
|
|
1354
|
+
mimeType,
|
|
1355
|
+
extension: extension2,
|
|
1356
|
+
data: base64Match[2] ?? ""
|
|
1357
|
+
};
|
|
1209
1358
|
}
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1359
|
+
const plainMatch = /^data:([^,]*?),(.*)$/i.exec(value);
|
|
1360
|
+
if (plainMatch) {
|
|
1361
|
+
const { mimeType, extension: extension2 } = mimeTypeAndExtensionFromMediaType(
|
|
1362
|
+
plainMatch[1] ?? "",
|
|
1363
|
+
"text/plain",
|
|
1364
|
+
"txt"
|
|
1365
|
+
);
|
|
1366
|
+
let decoded;
|
|
1367
|
+
try {
|
|
1368
|
+
decoded = decodeURIComponent(plainMatch[2] ?? "");
|
|
1369
|
+
} catch {
|
|
1370
|
+
throw new AiConnectError(
|
|
1371
|
+
"validation_error",
|
|
1372
|
+
"unsupported data URL encoding"
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
mimeType,
|
|
1377
|
+
extension: extension2,
|
|
1378
|
+
data: encodeBase64(new TextEncoder().encode(decoded))
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
if (/^data:/i.test(value)) {
|
|
1382
|
+
throw new AiConnectError(
|
|
1383
|
+
"validation_error",
|
|
1384
|
+
"unsupported data URL encoding"
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
return null;
|
|
1218
1388
|
}
|
|
1219
1389
|
function mimeTypeFromName(name) {
|
|
1220
1390
|
const ext = extension(name);
|
|
@@ -1428,9 +1598,61 @@ async function materializePortableFile(file) {
|
|
|
1428
1598
|
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1429
1599
|
}
|
|
1430
1600
|
}
|
|
1601
|
+
if (category === "other") {
|
|
1602
|
+
const base64 = await portableFileToBase64(file);
|
|
1603
|
+
if (base64 !== void 0) {
|
|
1604
|
+
payload.base64 = base64;
|
|
1605
|
+
payload.dataUrl = `data:${mimeType};base64,${base64}`;
|
|
1606
|
+
} else if (!uri && !providerFileId) {
|
|
1607
|
+
const droppedReason = `Portable file "${file.name}" (category "other", kind "${file.kind}") has no decodable byte source and no remote uri/providerFileId; its content was dropped and this payload carries metadata only.`;
|
|
1608
|
+
payload.droppedReason = droppedReason;
|
|
1609
|
+
payload.warnings = [...payload.warnings ?? [], droppedReason];
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1431
1612
|
return payload;
|
|
1432
1613
|
}
|
|
1433
|
-
function
|
|
1614
|
+
function collapsePathSegments(value) {
|
|
1615
|
+
const normalized = value.replaceAll("\\", "/");
|
|
1616
|
+
const isAbsolute = normalized.startsWith("/");
|
|
1617
|
+
const segments = normalized.split("/");
|
|
1618
|
+
const resolved = [];
|
|
1619
|
+
for (const segment of segments) {
|
|
1620
|
+
if (segment === "" || segment === ".") {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (segment === "..") {
|
|
1624
|
+
if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
|
|
1625
|
+
resolved.pop();
|
|
1626
|
+
} else if (!isAbsolute) {
|
|
1627
|
+
resolved.push("..");
|
|
1628
|
+
}
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
resolved.push(segment);
|
|
1632
|
+
}
|
|
1633
|
+
return (isAbsolute ? "/" : "") + resolved.join("/");
|
|
1634
|
+
}
|
|
1635
|
+
function applyPathGuard(rawPath, guard) {
|
|
1636
|
+
if (!guard) {
|
|
1637
|
+
return rawPath;
|
|
1638
|
+
}
|
|
1639
|
+
if (guard === "basename") {
|
|
1640
|
+
return basename(rawPath);
|
|
1641
|
+
}
|
|
1642
|
+
const resolved = collapsePathSegments(rawPath);
|
|
1643
|
+
const withinAllowedRoot = guard.allowedRoots.some((root) => {
|
|
1644
|
+
const resolvedRoot = collapsePathSegments(root);
|
|
1645
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}/`);
|
|
1646
|
+
});
|
|
1647
|
+
if (!withinAllowedRoot) {
|
|
1648
|
+
throw new AiConnectError(
|
|
1649
|
+
"validation_error",
|
|
1650
|
+
`Path "${rawPath}" is outside the configured allowedRoots for portable file attachments.`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
return rawPath;
|
|
1654
|
+
}
|
|
1655
|
+
function preparePortableFile(input, options) {
|
|
1434
1656
|
if (isPortableFile(input)) {
|
|
1435
1657
|
return input;
|
|
1436
1658
|
}
|
|
@@ -1456,11 +1678,12 @@ function preparePortableFile(input) {
|
|
|
1456
1678
|
}
|
|
1457
1679
|
};
|
|
1458
1680
|
}
|
|
1681
|
+
const guardedPath = applyPathGuard(input, options?.pathGuard);
|
|
1459
1682
|
return {
|
|
1460
|
-
id: `path:${
|
|
1683
|
+
id: `path:${guardedPath}`,
|
|
1461
1684
|
kind: "path",
|
|
1462
|
-
name: basename(
|
|
1463
|
-
source:
|
|
1685
|
+
name: basename(guardedPath),
|
|
1686
|
+
source: guardedPath
|
|
1464
1687
|
};
|
|
1465
1688
|
}
|
|
1466
1689
|
if (isRemoteReference(input)) {
|
|
@@ -1501,17 +1724,76 @@ function preparePortableFile(input) {
|
|
|
1501
1724
|
}
|
|
1502
1725
|
|
|
1503
1726
|
// src/logging.ts
|
|
1727
|
+
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
1728
|
+
"apikey",
|
|
1729
|
+
"authorization",
|
|
1730
|
+
"password",
|
|
1731
|
+
"token",
|
|
1732
|
+
"x-api-key",
|
|
1733
|
+
"x-goog-api-key"
|
|
1734
|
+
]);
|
|
1735
|
+
var REDACTED = "[REDACTED]";
|
|
1736
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1737
|
+
var SECRET_PREFIX_RE = /^(sk-|sk-ant-|xox[baprs]-|ghp_|gho_|AIza)/;
|
|
1738
|
+
function looksLikeSecretValue(value) {
|
|
1739
|
+
if (UUID_RE.test(value)) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
if (/^Bearer\s+/i.test(value)) {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
if (SECRET_PREFIX_RE.test(value)) {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
return /^[A-Za-z0-9+/=_]{40,}$/.test(value);
|
|
1749
|
+
}
|
|
1750
|
+
function redactNode(node) {
|
|
1751
|
+
if (node === null || node === void 0) {
|
|
1752
|
+
return node;
|
|
1753
|
+
}
|
|
1754
|
+
if (typeof node === "string") {
|
|
1755
|
+
return looksLikeSecretValue(node) ? REDACTED : node;
|
|
1756
|
+
}
|
|
1757
|
+
if (typeof node !== "object") {
|
|
1758
|
+
return node;
|
|
1759
|
+
}
|
|
1760
|
+
if (Array.isArray(node)) {
|
|
1761
|
+
return node.map((item) => redactNode(item));
|
|
1762
|
+
}
|
|
1763
|
+
const result = {};
|
|
1764
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1765
|
+
result[key] = REDACT_KEYS.has(key.toLowerCase()) ? REDACTED : redactNode(value);
|
|
1766
|
+
}
|
|
1767
|
+
return result;
|
|
1768
|
+
}
|
|
1769
|
+
function redactEvent(event) {
|
|
1770
|
+
return redactNode(event);
|
|
1771
|
+
}
|
|
1772
|
+
function redactBaseUrl(url) {
|
|
1773
|
+
try {
|
|
1774
|
+
const parsed = new URL(url);
|
|
1775
|
+
parsed.username = "";
|
|
1776
|
+
parsed.password = "";
|
|
1777
|
+
parsed.search = "";
|
|
1778
|
+
parsed.hash = "";
|
|
1779
|
+
return parsed.toString();
|
|
1780
|
+
} catch {
|
|
1781
|
+
const withoutHash = url.split("#")[0] ?? url;
|
|
1782
|
+
return withoutHash.split("?")[0] ?? withoutHash;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1504
1785
|
function createConsoleWideEventLogger(options = {}) {
|
|
1505
1786
|
const write = options.write ?? ((line) => {
|
|
1506
1787
|
console.info(line);
|
|
1507
1788
|
});
|
|
1508
1789
|
return (event) => {
|
|
1790
|
+
const redacted = redactEvent(event);
|
|
1509
1791
|
write(
|
|
1510
|
-
options.pretty ? JSON.stringify(
|
|
1792
|
+
options.pretty ? JSON.stringify(redacted, null, 2) : JSON.stringify(redacted)
|
|
1511
1793
|
);
|
|
1512
1794
|
};
|
|
1513
1795
|
}
|
|
1514
|
-
async function shouldEmitWideEvent(event, options = {}) {
|
|
1796
|
+
async function shouldEmitWideEvent(event, options = {}, random = Math.random) {
|
|
1515
1797
|
const sampleRate = options.sampleRate ?? 1;
|
|
1516
1798
|
if ((options.keepErrors ?? true) && event.outcome === "error") {
|
|
1517
1799
|
return true;
|
|
@@ -1545,7 +1827,7 @@ async function shouldEmitWideEvent(event, options = {}) {
|
|
|
1545
1827
|
if (sampleRate >= 1) {
|
|
1546
1828
|
return true;
|
|
1547
1829
|
}
|
|
1548
|
-
return
|
|
1830
|
+
return random() < sampleRate;
|
|
1549
1831
|
}
|
|
1550
1832
|
|
|
1551
1833
|
// src/router.ts
|
|
@@ -1610,6 +1892,19 @@ function buildWeightedOrder(routes) {
|
|
|
1610
1892
|
}
|
|
1611
1893
|
return expanded;
|
|
1612
1894
|
}
|
|
1895
|
+
var ROUTING_STRATEGIES = {
|
|
1896
|
+
priority: (ordered) => ordered,
|
|
1897
|
+
failover: (ordered) => ordered,
|
|
1898
|
+
"round-robin": (ordered, cursor, cursorKey) => {
|
|
1899
|
+
const index = cursor(cursorKey, ordered.length);
|
|
1900
|
+
return rotate(ordered, index);
|
|
1901
|
+
},
|
|
1902
|
+
"weighted-round-robin": (ordered, cursor, cursorKey) => {
|
|
1903
|
+
const weighted = buildWeightedOrder(ordered);
|
|
1904
|
+
const index = cursor(cursorKey, weighted.length);
|
|
1905
|
+
return unique(rotate(weighted, index));
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1613
1908
|
var RouteRegistry = class {
|
|
1614
1909
|
config;
|
|
1615
1910
|
runtime;
|
|
@@ -1680,14 +1975,21 @@ var RouteRegistry = class {
|
|
|
1680
1975
|
const routeIds = request.routeHints?.pool?.length ? resolveRouteSelectors(request.routeHints.pool, this.config.routes) : operationRouting.pool;
|
|
1681
1976
|
const excludedIds = new Set(request.routeHints?.excludeRouteIds ?? []);
|
|
1682
1977
|
const requestedModel = request.routeHints?.model;
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1978
|
+
const eligible = routeIds.map((routeId) => this.#routeMap.get(routeId)).filter((route) => Boolean(route)).filter((route) => !excludedIds.has(route.id)).filter((route) => runtimeEligible(route, this.runtime)).filter((route) => capabilityMatch(route, request.routeHints)).map((route) => applyRequestedModel(route, requestedModel)).filter((route) => Boolean(route));
|
|
1979
|
+
const strategy = request.routeHints?.strategy ?? operationRouting.strategy;
|
|
1980
|
+
const cursorKey = `${operation}:${strategy}:${eligible.map((route) => route.id).join("|")}`;
|
|
1981
|
+
const healthyIds = new Set(
|
|
1982
|
+
eligible.filter((route) => {
|
|
1983
|
+
const state = this.getHealth(route).state;
|
|
1984
|
+
return state !== "cooling_down" && state !== "unavailable";
|
|
1985
|
+
}).map((route) => route.id)
|
|
1986
|
+
);
|
|
1687
1987
|
return this.#order(
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1988
|
+
strategy,
|
|
1989
|
+
eligible,
|
|
1990
|
+
healthyIds,
|
|
1991
|
+
request.routeHints?.preferredProviders,
|
|
1992
|
+
cursorKey
|
|
1691
1993
|
);
|
|
1692
1994
|
}
|
|
1693
1995
|
recordSuccess(route) {
|
|
@@ -1720,20 +2022,12 @@ var RouteRegistry = class {
|
|
|
1720
2022
|
failureCount
|
|
1721
2023
|
});
|
|
1722
2024
|
}
|
|
1723
|
-
#order(strategy,
|
|
1724
|
-
const sorted = baseSort(
|
|
1725
|
-
const
|
|
1726
|
-
const ordered =
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
if (strategy === "round-robin") {
|
|
1731
|
-
const index2 = this.#cursor(cursorKey, ordered.length);
|
|
1732
|
-
return rotate(ordered, index2);
|
|
1733
|
-
}
|
|
1734
|
-
const weighted = buildWeightedOrder(ordered);
|
|
1735
|
-
const index = this.#cursor(cursorKey, weighted.length);
|
|
1736
|
-
return unique(rotate(weighted, index));
|
|
2025
|
+
#order(strategy, eligible, healthyIds, preferredProviders, cursorKey) {
|
|
2026
|
+
const sorted = baseSort(eligible, preferredProviders);
|
|
2027
|
+
const orderedFull = this.config.routing.shuffleOnInit ? this.#shuffle(cursorKey, sorted) : sorted;
|
|
2028
|
+
const ordered = orderedFull.filter((route) => healthyIds.has(route.id));
|
|
2029
|
+
const strategyFn = ROUTING_STRATEGIES[strategy];
|
|
2030
|
+
return strategyFn(ordered, (key, length) => this.#cursor(key, length), cursorKey);
|
|
1737
2031
|
}
|
|
1738
2032
|
#shuffle(key, routes) {
|
|
1739
2033
|
const cached = this.#shuffles.get(key);
|
|
@@ -1837,8 +2131,22 @@ var MODEL_REFERENCE = {
|
|
|
1837
2131
|
"gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
|
|
1838
2132
|
"gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
|
|
1839
2133
|
};
|
|
2134
|
+
var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2135
|
+
"minimal",
|
|
2136
|
+
"low",
|
|
2137
|
+
"medium",
|
|
2138
|
+
"high",
|
|
2139
|
+
"xhigh"
|
|
2140
|
+
]);
|
|
1840
2141
|
function normalizeModelKey(model) {
|
|
1841
2142
|
let key = model.trim().toLowerCase();
|
|
2143
|
+
const lastSlashIndex = key.lastIndexOf("/");
|
|
2144
|
+
if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
|
|
2145
|
+
const tail = key.slice(lastSlashIndex + 1);
|
|
2146
|
+
if (REASONING_EFFORT_SUFFIXES.has(tail)) {
|
|
2147
|
+
key = key.slice(0, lastSlashIndex);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
1842
2150
|
const slashIndex = key.indexOf("/");
|
|
1843
2151
|
if (slashIndex > 0) {
|
|
1844
2152
|
key = key.slice(slashIndex + 1);
|
|
@@ -1846,6 +2154,7 @@ function normalizeModelKey(model) {
|
|
|
1846
2154
|
key = key.replace(/:(?:free|beta)$/i, "");
|
|
1847
2155
|
return key.trim();
|
|
1848
2156
|
}
|
|
2157
|
+
var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
|
|
1849
2158
|
function lookupModelRef(model) {
|
|
1850
2159
|
const normalized = normalizeModelKey(model);
|
|
1851
2160
|
if (!normalized) {
|
|
@@ -1868,7 +2177,7 @@ function lookupModelRef(model) {
|
|
|
1868
2177
|
}
|
|
1869
2178
|
let substringWinner;
|
|
1870
2179
|
for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
|
|
1871
|
-
if (normalized.includes(key)) {
|
|
2180
|
+
if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
|
|
1872
2181
|
if (!substringWinner || key.length > substringWinner.key.length) {
|
|
1873
2182
|
substringWinner = entry;
|
|
1874
2183
|
}
|
|
@@ -2103,6 +2412,14 @@ var DEFAULT_LOGGING_SERVICE = "ai-connect";
|
|
|
2103
2412
|
function splitCredentialValues(value, delimiter2) {
|
|
2104
2413
|
return value.split(delimiter2).map((item) => item.trim()).filter(Boolean);
|
|
2105
2414
|
}
|
|
2415
|
+
var ClientToolExecutionError = class extends Error {
|
|
2416
|
+
aiError;
|
|
2417
|
+
constructor(aiError) {
|
|
2418
|
+
super(aiError.message);
|
|
2419
|
+
this.name = "ClientToolExecutionError";
|
|
2420
|
+
this.aiError = aiError;
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2106
2423
|
function materializeRuntimeConfig(config, runtime) {
|
|
2107
2424
|
const routeExpansion = /* @__PURE__ */ new Map();
|
|
2108
2425
|
const routes = [];
|
|
@@ -2117,13 +2434,14 @@ function materializeRuntimeConfig(config, runtime) {
|
|
|
2117
2434
|
continue;
|
|
2118
2435
|
}
|
|
2119
2436
|
const expandedIds = [];
|
|
2437
|
+
const expandedWeight = Math.max(1, Math.floor(route.weight / values.length));
|
|
2120
2438
|
values.forEach((apiKey, index) => {
|
|
2121
2439
|
const credentialId = `${route.credentialId}#${index + 1}`;
|
|
2122
2440
|
const expandedRoute = {
|
|
2123
2441
|
...route,
|
|
2124
2442
|
id: `${route.id}#${index + 1}`,
|
|
2125
2443
|
credentialId,
|
|
2126
|
-
weight:
|
|
2444
|
+
weight: expandedWeight,
|
|
2127
2445
|
credential: {
|
|
2128
2446
|
...route.credential,
|
|
2129
2447
|
id: credentialId,
|
|
@@ -2164,6 +2482,27 @@ async function sleep(delayMs) {
|
|
|
2164
2482
|
}
|
|
2165
2483
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2166
2484
|
}
|
|
2485
|
+
async function runWithAbortRace(handlerPromise, abort) {
|
|
2486
|
+
if (abort.signal.aborted) {
|
|
2487
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2488
|
+
});
|
|
2489
|
+
throw mapAbortError(abort.reason);
|
|
2490
|
+
}
|
|
2491
|
+
let onAbort;
|
|
2492
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2493
|
+
onAbort = () => reject(mapAbortError(abort.reason));
|
|
2494
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2495
|
+
});
|
|
2496
|
+
try {
|
|
2497
|
+
return await Promise.race([handlerPromise, abortPromise]);
|
|
2498
|
+
} finally {
|
|
2499
|
+
if (onAbort) {
|
|
2500
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
2501
|
+
}
|
|
2502
|
+
void Promise.resolve(handlerPromise).catch(() => {
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2167
2506
|
function normalizeWorkingDirectory(value) {
|
|
2168
2507
|
if (value === void 0) {
|
|
2169
2508
|
return void 0;
|
|
@@ -2205,6 +2544,12 @@ function normalizeClientToolRegistry(tools) {
|
|
|
2205
2544
|
const registry = /* @__PURE__ */ new Map();
|
|
2206
2545
|
for (const tool of tools ?? []) {
|
|
2207
2546
|
const normalized = normalizeClientToolDefinition(tool);
|
|
2547
|
+
if (registry.has(normalized.function.name)) {
|
|
2548
|
+
throw new AiConnectError(
|
|
2549
|
+
"validation_error",
|
|
2550
|
+
`Duplicate client tool name "${normalized.function.name}" registered on this client.`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2208
2553
|
registry.set(normalized.function.name, normalized);
|
|
2209
2554
|
}
|
|
2210
2555
|
return registry;
|
|
@@ -2230,10 +2575,22 @@ function resolveClientToolSelections(selections, registry) {
|
|
|
2230
2575
|
`No client tool named "${name}" is registered on this client.`
|
|
2231
2576
|
);
|
|
2232
2577
|
}
|
|
2578
|
+
if (resolved.has(name)) {
|
|
2579
|
+
throw new AiConnectError(
|
|
2580
|
+
"validation_error",
|
|
2581
|
+
`Duplicate client tool "${name}" in the clientTools selection.`
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2233
2584
|
resolved.set(name, registered);
|
|
2234
2585
|
continue;
|
|
2235
2586
|
}
|
|
2236
2587
|
const normalized = normalizeClientToolDefinition(selection);
|
|
2588
|
+
if (resolved.has(normalized.function.name)) {
|
|
2589
|
+
throw new AiConnectError(
|
|
2590
|
+
"validation_error",
|
|
2591
|
+
`Duplicate client tool "${normalized.function.name}" in the clientTools selection.`
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2237
2594
|
resolved.set(normalized.function.name, normalized);
|
|
2238
2595
|
}
|
|
2239
2596
|
return Array.from(resolved.values());
|
|
@@ -2346,6 +2703,34 @@ function requiresImageInput(request) {
|
|
|
2346
2703
|
function requiresFileUpload(request) {
|
|
2347
2704
|
return request.attachments.some((file) => isPortableDocumentFile(file));
|
|
2348
2705
|
}
|
|
2706
|
+
function resolveRequiredCapabilities(request) {
|
|
2707
|
+
const requiresSchema = requiresToolSchema(request);
|
|
2708
|
+
const requiresClientTools = requiresClientToolExecution(request);
|
|
2709
|
+
const requiresImage = requiresImageInput(request);
|
|
2710
|
+
const requiresFile = requiresFileUpload(request);
|
|
2711
|
+
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2712
|
+
const projected = requiresAnyCapability ? {
|
|
2713
|
+
...request,
|
|
2714
|
+
routeHints: {
|
|
2715
|
+
...request.routeHints ?? {},
|
|
2716
|
+
requiredCapabilities: {
|
|
2717
|
+
...request.routeHints?.requiredCapabilities ?? {},
|
|
2718
|
+
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2719
|
+
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2720
|
+
...requiresImage ? { supportsImageInput: true } : {},
|
|
2721
|
+
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
} : request;
|
|
2725
|
+
return {
|
|
2726
|
+
requiresSchema,
|
|
2727
|
+
requiresClientTools,
|
|
2728
|
+
requiresImage,
|
|
2729
|
+
requiresFile,
|
|
2730
|
+
requiresAnyCapability,
|
|
2731
|
+
request: projected
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2349
2734
|
function normalizeRequestBase(request) {
|
|
2350
2735
|
if (request.messages.length === 0) {
|
|
2351
2736
|
throw new AiConnectError("validation_error", "Unified generate/stream requests must include at least one message.");
|
|
@@ -2554,7 +2939,10 @@ function summarizeRoute(route) {
|
|
|
2554
2939
|
model: route.model,
|
|
2555
2940
|
handlerKey: route.handlerKey,
|
|
2556
2941
|
profileId: route.profileId,
|
|
2557
|
-
|
|
2942
|
+
// SYS-3 / C1a: never emit a raw baseUrl into a wide event — a token embedded in
|
|
2943
|
+
// userinfo (`https://user:secret@host`) or the query string would leak verbatim,
|
|
2944
|
+
// unlike the secret-safe toPublicRoute projection. Strip credentials first.
|
|
2945
|
+
...route.transport.baseUrl ? { baseUrl: redactBaseUrl(route.transport.baseUrl) } : {}
|
|
2558
2946
|
};
|
|
2559
2947
|
}
|
|
2560
2948
|
function summarizeFiles(files) {
|
|
@@ -2865,21 +3253,40 @@ function createClient(configOrInput, options = {}) {
|
|
|
2865
3253
|
for (const toolCall of toolCalls) {
|
|
2866
3254
|
const tool = registry.get(toolCall.name);
|
|
2867
3255
|
if (!tool) {
|
|
2868
|
-
throw new
|
|
2869
|
-
|
|
2870
|
-
|
|
3256
|
+
throw new ClientToolExecutionError(
|
|
3257
|
+
new AiConnectError(
|
|
3258
|
+
"validation_error",
|
|
3259
|
+
`No client tool handler is registered for "${toolCall.name}".`
|
|
3260
|
+
)
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
if (abort.signal.aborted) {
|
|
3264
|
+
throw mapAbortError(abort.reason);
|
|
3265
|
+
}
|
|
3266
|
+
let execution;
|
|
3267
|
+
try {
|
|
3268
|
+
execution = normalizeToolResult(
|
|
3269
|
+
await tool.execute(toolCall.arguments, {
|
|
3270
|
+
route,
|
|
3271
|
+
request,
|
|
3272
|
+
runtime,
|
|
3273
|
+
toolCall,
|
|
3274
|
+
messages,
|
|
3275
|
+
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
3276
|
+
})
|
|
3277
|
+
);
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
if (abort.signal.aborted) {
|
|
3280
|
+
throw mapAbortError(abort.reason);
|
|
3281
|
+
}
|
|
3282
|
+
throw new ClientToolExecutionError(
|
|
3283
|
+
new AiConnectError(
|
|
3284
|
+
"validation_error",
|
|
3285
|
+
`Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
3286
|
+
{ cause: error }
|
|
3287
|
+
)
|
|
2871
3288
|
);
|
|
2872
3289
|
}
|
|
2873
|
-
const execution = normalizeToolResult(
|
|
2874
|
-
await tool.execute(toolCall.arguments, {
|
|
2875
|
-
route,
|
|
2876
|
-
request,
|
|
2877
|
-
runtime,
|
|
2878
|
-
toolCall,
|
|
2879
|
-
messages,
|
|
2880
|
-
...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
|
|
2881
|
-
})
|
|
2882
|
-
);
|
|
2883
3290
|
toolMessages.push({
|
|
2884
3291
|
role: "tool",
|
|
2885
3292
|
content: execution.content,
|
|
@@ -2893,28 +3300,33 @@ function createClient(configOrInput, options = {}) {
|
|
|
2893
3300
|
if (abort.signal.aborted) {
|
|
2894
3301
|
throw mapAbortError(abort.reason);
|
|
2895
3302
|
}
|
|
2896
|
-
output = await
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3303
|
+
output = await runWithAbortRace(
|
|
3304
|
+
handler({
|
|
3305
|
+
route,
|
|
3306
|
+
request: {
|
|
3307
|
+
...request,
|
|
3308
|
+
messages,
|
|
3309
|
+
attachments: []
|
|
3310
|
+
},
|
|
3311
|
+
runtime,
|
|
3312
|
+
attempt: baseAttempt,
|
|
3313
|
+
abort,
|
|
3314
|
+
telemetry: {
|
|
3315
|
+
captureTransport(summary) {
|
|
3316
|
+
transportSummary2 = summary;
|
|
3317
|
+
}
|
|
2909
3318
|
}
|
|
2910
|
-
}
|
|
2911
|
-
|
|
3319
|
+
}),
|
|
3320
|
+
abort
|
|
3321
|
+
);
|
|
2912
3322
|
warnings.push(...output.warnings ?? []);
|
|
2913
3323
|
usage = mergeUsage(usage, output.usage);
|
|
2914
3324
|
}
|
|
2915
|
-
throw new
|
|
2916
|
-
|
|
2917
|
-
|
|
3325
|
+
throw new ClientToolExecutionError(
|
|
3326
|
+
new AiConnectError(
|
|
3327
|
+
"validation_error",
|
|
3328
|
+
"clientTools exceeded the maximum tool-call loop depth of 8."
|
|
3329
|
+
)
|
|
2918
3330
|
);
|
|
2919
3331
|
}
|
|
2920
3332
|
async function emitWideEvent(event) {
|
|
@@ -2945,26 +3357,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
2945
3357
|
async function executeGenerateOperation(normalizedRequest, operationName, abort, seed = {}) {
|
|
2946
3358
|
const startedAt = seed.startMs ?? nowMs(runtime);
|
|
2947
3359
|
const requestId = seed.requestId ?? resolveEventRequestId(normalizedRequest.logContext);
|
|
2948
|
-
const
|
|
2949
|
-
const
|
|
2950
|
-
const requiresImage = requiresImageInput(normalizedRequest);
|
|
2951
|
-
const requiresFile = requiresFileUpload(normalizedRequest);
|
|
2952
|
-
const requiresAnyCapability = requiresSchema || requiresClientTools || requiresImage || requiresFile;
|
|
2953
|
-
const resolvedCandidates = router.resolveCandidates(
|
|
2954
|
-
requiresAnyCapability ? {
|
|
2955
|
-
...normalizedRequest,
|
|
2956
|
-
routeHints: {
|
|
2957
|
-
...normalizedRequest.routeHints ?? {},
|
|
2958
|
-
requiredCapabilities: {
|
|
2959
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
2960
|
-
...requiresSchema ? { supportsToolSchema: true } : {},
|
|
2961
|
-
...requiresClientTools ? { supportsClientToolExecution: true } : {},
|
|
2962
|
-
...requiresImage ? { supportsImageInput: true } : {},
|
|
2963
|
-
...requiresFile ? { supportsFileUpload: true } : {}
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
} : normalizedRequest
|
|
2967
|
-
);
|
|
3360
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
3361
|
+
const resolvedCandidates = router.resolveCandidates(capabilityScopedRequest);
|
|
2968
3362
|
const candidates = seed.candidates ?? resolvedCandidates.map(summarizeRoute);
|
|
2969
3363
|
const requestSummary = summarizeRequest(normalizedRequest, candidates);
|
|
2970
3364
|
if (resolvedCandidates.length === 0) {
|
|
@@ -3052,18 +3446,21 @@ function createClient(configOrInput, options = {}) {
|
|
|
3052
3446
|
const attemptStartedAt = nowMs(runtime);
|
|
3053
3447
|
let transportSummary2;
|
|
3054
3448
|
try {
|
|
3055
|
-
let output = await
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3449
|
+
let output = await runWithAbortRace(
|
|
3450
|
+
handler.generate({
|
|
3451
|
+
route,
|
|
3452
|
+
request: routeRequest,
|
|
3453
|
+
runtime,
|
|
3454
|
+
attempt: attempts.length + 1,
|
|
3455
|
+
abort,
|
|
3456
|
+
telemetry: {
|
|
3457
|
+
captureTransport(summary) {
|
|
3458
|
+
transportSummary2 = summary;
|
|
3459
|
+
}
|
|
3064
3460
|
}
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3461
|
+
}),
|
|
3462
|
+
abort
|
|
3463
|
+
);
|
|
3067
3464
|
if (routeRequest.clientTools?.length && output.toolCalls?.length) {
|
|
3068
3465
|
const loopResult = await executeClientToolLoop(
|
|
3069
3466
|
route,
|
|
@@ -3078,6 +3475,9 @@ function createClient(configOrInput, options = {}) {
|
|
|
3078
3475
|
transportSummary2 = loopResult.transportSummary;
|
|
3079
3476
|
}
|
|
3080
3477
|
}
|
|
3478
|
+
if (abort.signal.aborted) {
|
|
3479
|
+
throw mapAbortError(abort.reason);
|
|
3480
|
+
}
|
|
3081
3481
|
wideAttempts.push({
|
|
3082
3482
|
index: wideAttempts.length + 1,
|
|
3083
3483
|
route: routeSummary,
|
|
@@ -3148,6 +3548,49 @@ function createClient(configOrInput, options = {}) {
|
|
|
3148
3548
|
routeId: route.id
|
|
3149
3549
|
});
|
|
3150
3550
|
}
|
|
3551
|
+
if (error instanceof ClientToolExecutionError) {
|
|
3552
|
+
const toolError = error.aiError;
|
|
3553
|
+
attempts.push({
|
|
3554
|
+
routeId: route.id,
|
|
3555
|
+
handlerKey: route.handlerKey,
|
|
3556
|
+
errorCode: toolError.code,
|
|
3557
|
+
message: toolError.message
|
|
3558
|
+
});
|
|
3559
|
+
wideAttempts.push({
|
|
3560
|
+
index: wideAttempts.length + 1,
|
|
3561
|
+
route: routeSummary,
|
|
3562
|
+
durationMs: nowMs(runtime) - attemptStartedAt,
|
|
3563
|
+
outcome: "error",
|
|
3564
|
+
retryCount,
|
|
3565
|
+
errorCode: toolError.code,
|
|
3566
|
+
message: toolError.message,
|
|
3567
|
+
...transportSummary2 ? { transport: transportSummary2 } : {}
|
|
3568
|
+
});
|
|
3569
|
+
await emitWideEvent({
|
|
3570
|
+
timestamp: isoTimestamp(startedAt),
|
|
3571
|
+
event: "ai_connect.operation",
|
|
3572
|
+
requestId,
|
|
3573
|
+
operationName,
|
|
3574
|
+
operation: normalizedRequest.operation,
|
|
3575
|
+
runtime: runtime.kind,
|
|
3576
|
+
outcome: "error",
|
|
3577
|
+
durationMs: nowMs(runtime) - startedAt,
|
|
3578
|
+
candidates,
|
|
3579
|
+
attempts: wideAttempts,
|
|
3580
|
+
request: requestSummary,
|
|
3581
|
+
error: {
|
|
3582
|
+
code: toolError.code,
|
|
3583
|
+
message: toolError.message,
|
|
3584
|
+
routeId: route.id
|
|
3585
|
+
},
|
|
3586
|
+
...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
|
|
3587
|
+
});
|
|
3588
|
+
throw new AiConnectError(toolError.code, toolError.message, {
|
|
3589
|
+
...toolError.details,
|
|
3590
|
+
attempts,
|
|
3591
|
+
routeId: route.id
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3151
3594
|
const normalizedError = toAiConnectError(error);
|
|
3152
3595
|
const actions = config.routing.fallback.on[normalizedError.code] ?? [];
|
|
3153
3596
|
attempts.push({
|
|
@@ -3392,30 +3835,16 @@ function createClient(configOrInput, options = {}) {
|
|
|
3392
3835
|
return report;
|
|
3393
3836
|
}
|
|
3394
3837
|
async function runPingWithAbort(route, handler, abort) {
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3838
|
+
return runWithAbortRace(
|
|
3839
|
+
handler({
|
|
3840
|
+
route,
|
|
3841
|
+
request: buildPingRequest(),
|
|
3842
|
+
runtime,
|
|
3843
|
+
attempt: 1,
|
|
3844
|
+
abort
|
|
3845
|
+
}),
|
|
3403
3846
|
abort
|
|
3404
|
-
|
|
3405
|
-
let onAbort;
|
|
3406
|
-
const abortPromise = new Promise((_, reject) => {
|
|
3407
|
-
onAbort = () => reject(mapAbortError(abort.reason));
|
|
3408
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
3409
|
-
});
|
|
3410
|
-
try {
|
|
3411
|
-
return await Promise.race([handlerPromise, abortPromise]);
|
|
3412
|
-
} finally {
|
|
3413
|
-
if (onAbort) {
|
|
3414
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
3415
|
-
}
|
|
3416
|
-
void Promise.resolve(handlerPromise).catch(() => {
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3847
|
+
);
|
|
3419
3848
|
}
|
|
3420
3849
|
async function checkEndpointReachable(route, handler, abort) {
|
|
3421
3850
|
if (abort.signal.aborted) {
|
|
@@ -3983,19 +4412,8 @@ function createClient(configOrInput, options = {}) {
|
|
|
3983
4412
|
release = await limiter.acquire(abort.signal);
|
|
3984
4413
|
const selectedModel = await resolveSelectedModel(preparedRequest);
|
|
3985
4414
|
const normalizedRequest = withSelectedModel(preparedRequest, selectedModel);
|
|
3986
|
-
const
|
|
3987
|
-
const candidates = router.resolveCandidates(
|
|
3988
|
-
requiresSchema ? {
|
|
3989
|
-
...normalizedRequest,
|
|
3990
|
-
routeHints: {
|
|
3991
|
-
...normalizedRequest.routeHints ?? {},
|
|
3992
|
-
requiredCapabilities: {
|
|
3993
|
-
...normalizedRequest.routeHints?.requiredCapabilities ?? {},
|
|
3994
|
-
supportsToolSchema: true
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
} : normalizedRequest
|
|
3998
|
-
);
|
|
4415
|
+
const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
|
|
4416
|
+
const candidates = router.resolveCandidates(capabilityScopedRequest);
|
|
3999
4417
|
const startedAt = nowMs(runtime);
|
|
4000
4418
|
const requestId = resolveEventRequestId(normalizedRequest.logContext);
|
|
4001
4419
|
const candidateSummaries = candidates.map(summarizeRoute);
|
|
@@ -4003,7 +4421,10 @@ function createClient(configOrInput, options = {}) {
|
|
|
4003
4421
|
const attempts = [];
|
|
4004
4422
|
const wideAttempts = [];
|
|
4005
4423
|
if (candidates.length === 0) {
|
|
4006
|
-
const error =
|
|
4424
|
+
const error = requiresImage || requiresFile ? new AiConnectError(
|
|
4425
|
+
"unsupported_capability",
|
|
4426
|
+
`No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
|
|
4427
|
+
) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
|
|
4007
4428
|
await emitWideEvent({
|
|
4008
4429
|
timestamp: isoTimestamp(startedAt),
|
|
4009
4430
|
event: "ai_connect.operation",
|
|
@@ -4479,12 +4900,32 @@ function trimTrailingSlash(value) {
|
|
|
4479
4900
|
function appendPath(baseUrl, suffix) {
|
|
4480
4901
|
return `${trimTrailingSlash(baseUrl)}/${suffix.replace(/^\/+/, "")}`;
|
|
4481
4902
|
}
|
|
4903
|
+
function isAbortError(error) {
|
|
4904
|
+
if (error instanceof AiConnectError) {
|
|
4905
|
+
return error.code === "aborted";
|
|
4906
|
+
}
|
|
4907
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
4908
|
+
}
|
|
4909
|
+
function attachPartsToLastUserTurn(conversation, createEmptyUserTurn) {
|
|
4910
|
+
let targetIndex = -1;
|
|
4911
|
+
for (let index = conversation.length - 1; index >= 0; index -= 1) {
|
|
4912
|
+
if (conversation[index]?.role === "user") {
|
|
4913
|
+
targetIndex = index;
|
|
4914
|
+
break;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
if (targetIndex === -1) {
|
|
4918
|
+
conversation.push(createEmptyUserTurn());
|
|
4919
|
+
targetIndex = conversation.length - 1;
|
|
4920
|
+
}
|
|
4921
|
+
return conversation[targetIndex];
|
|
4922
|
+
}
|
|
4482
4923
|
function resolveOpenAiEndpoint(route, operation) {
|
|
4483
4924
|
const configured = route.transport.baseUrl?.trim();
|
|
4484
4925
|
if (!configured) {
|
|
4485
4926
|
return operation === "image" ? "https://api.openai.com/v1/images/generations" : "https://api.openai.com/v1/chat/completions";
|
|
4486
4927
|
}
|
|
4487
|
-
if (configured.endsWith("/chat/completions") || configured.endsWith("/images/generations")) {
|
|
4928
|
+
if (operation === "text" && configured.endsWith("/chat/completions") || operation === "image" && configured.endsWith("/images/generations")) {
|
|
4488
4929
|
return configured;
|
|
4489
4930
|
}
|
|
4490
4931
|
return appendPath(
|
|
@@ -5315,7 +5756,7 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5315
5756
|
}
|
|
5316
5757
|
try {
|
|
5317
5758
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5318
|
-
const root = base ? base.replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5759
|
+
const root = base ? trimTrailingSlash(base).replace(/\/(messages|models)$/, "") : "https://api.anthropic.com/v1";
|
|
5319
5760
|
const endpoint = appendPath(root, "/files");
|
|
5320
5761
|
const form = new FormData();
|
|
5321
5762
|
form.append(
|
|
@@ -5339,7 +5780,10 @@ async function uploadAnthropicFile(payload, uploader) {
|
|
|
5339
5780
|
}
|
|
5340
5781
|
const id = recordOf(data).id;
|
|
5341
5782
|
return typeof id === "string" ? id : void 0;
|
|
5342
|
-
} catch {
|
|
5783
|
+
} catch (error) {
|
|
5784
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5785
|
+
throw error;
|
|
5786
|
+
}
|
|
5343
5787
|
return void 0;
|
|
5344
5788
|
}
|
|
5345
5789
|
}
|
|
@@ -5372,7 +5816,10 @@ async function uploadOpenAiFile(payload, uploader) {
|
|
|
5372
5816
|
}
|
|
5373
5817
|
const id = recordOf(data).id;
|
|
5374
5818
|
return typeof id === "string" ? id : void 0;
|
|
5375
|
-
} catch {
|
|
5819
|
+
} catch (error) {
|
|
5820
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5821
|
+
throw error;
|
|
5822
|
+
}
|
|
5376
5823
|
return void 0;
|
|
5377
5824
|
}
|
|
5378
5825
|
}
|
|
@@ -5382,10 +5829,11 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5382
5829
|
}
|
|
5383
5830
|
try {
|
|
5384
5831
|
const base = uploader.context.route.transport.baseUrl?.trim();
|
|
5385
|
-
const
|
|
5386
|
-
|
|
5832
|
+
const root = base ? trimTrailingSlash(base.replace(/\/v1beta\/models.*$/, "/v1beta")) : void 0;
|
|
5833
|
+
const endpoint = root ? appendPath(
|
|
5834
|
+
root.endsWith("/v1beta") ? `${root.slice(0, -"/v1beta".length)}/upload/v1beta` : `${root}/upload`,
|
|
5387
5835
|
"/files"
|
|
5388
|
-
)
|
|
5836
|
+
) : "https://generativelanguage.googleapis.com/upload/v1beta/files";
|
|
5389
5837
|
const response = await uploader.fetchImpl(endpoint, {
|
|
5390
5838
|
method: "POST",
|
|
5391
5839
|
headers: {
|
|
@@ -5404,7 +5852,10 @@ async function uploadGeminiFile(payload, uploader) {
|
|
|
5404
5852
|
const file = recordOf(recordOf(data).file);
|
|
5405
5853
|
const uri = file.uri ?? recordOf(data).uri;
|
|
5406
5854
|
return typeof uri === "string" ? uri : void 0;
|
|
5407
|
-
} catch {
|
|
5855
|
+
} catch (error) {
|
|
5856
|
+
if (uploader.context.abort.signal.aborted || isAbortError(error)) {
|
|
5857
|
+
throw error;
|
|
5858
|
+
}
|
|
5408
5859
|
return void 0;
|
|
5409
5860
|
}
|
|
5410
5861
|
}
|
|
@@ -5602,21 +6053,10 @@ function attachOpenAiParts(conversation, parts) {
|
|
|
5602
6053
|
if (parts.length === 0) {
|
|
5603
6054
|
return conversation;
|
|
5604
6055
|
}
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
break;
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
if (targetIndex === -1) {
|
|
5613
|
-
conversation.push({
|
|
5614
|
-
role: "user",
|
|
5615
|
-
content: ""
|
|
5616
|
-
});
|
|
5617
|
-
targetIndex = conversation.length - 1;
|
|
5618
|
-
}
|
|
5619
|
-
const target = conversation[targetIndex];
|
|
6056
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6057
|
+
role: "user",
|
|
6058
|
+
content: ""
|
|
6059
|
+
}));
|
|
5620
6060
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5621
6061
|
target.content = [...baseParts, ...parts];
|
|
5622
6062
|
return conversation;
|
|
@@ -5702,21 +6142,10 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5702
6142
|
}
|
|
5703
6143
|
const attachmentParts = await anthropicAttachmentParts(attachments, uploader);
|
|
5704
6144
|
if (attachmentParts.length > 0) {
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
break;
|
|
5710
|
-
}
|
|
5711
|
-
}
|
|
5712
|
-
if (targetIndex === -1) {
|
|
5713
|
-
conversation.push({
|
|
5714
|
-
role: "user",
|
|
5715
|
-
content: ""
|
|
5716
|
-
});
|
|
5717
|
-
targetIndex = conversation.length - 1;
|
|
5718
|
-
}
|
|
5719
|
-
const target = conversation[targetIndex];
|
|
6145
|
+
const target = attachPartsToLastUserTurn(conversation, () => ({
|
|
6146
|
+
role: "user",
|
|
6147
|
+
content: ""
|
|
6148
|
+
}));
|
|
5720
6149
|
const baseParts = typeof target.content === "string" && target.content.trim().length > 0 ? [{ type: "text", text: target.content }] : Array.isArray(target.content) ? target.content : [];
|
|
5721
6150
|
target.content = [...baseParts, ...attachmentParts];
|
|
5722
6151
|
}
|
|
@@ -5727,12 +6156,27 @@ async function anthropicRequest(messages, attachments, uploader) {
|
|
|
5727
6156
|
}
|
|
5728
6157
|
async function geminiRequest(messages, attachments, uploader) {
|
|
5729
6158
|
const system = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n");
|
|
6159
|
+
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
6160
|
+
for (const message of messages) {
|
|
6161
|
+
if (hasAssistantToolCalls(message)) {
|
|
6162
|
+
for (const call of message.toolCalls) {
|
|
6163
|
+
toolNameByCallId.set(call.id, call.name);
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
5730
6167
|
const contents = messages.filter((message) => message.role !== "system").reduce(
|
|
5731
6168
|
(conversation, message) => {
|
|
5732
6169
|
if (isToolMessage(message)) {
|
|
6170
|
+
const resolvedName = message.toolName ?? toolNameByCallId.get(message.toolCallId);
|
|
6171
|
+
if (!resolvedName) {
|
|
6172
|
+
throw new AiConnectError(
|
|
6173
|
+
"validation_error",
|
|
6174
|
+
`Gemini functionResponse requires a tool name: the tool message for toolCallId "${message.toolCallId}" has no toolName and no matching assistant toolCall was found.`
|
|
6175
|
+
);
|
|
6176
|
+
}
|
|
5733
6177
|
const response = {
|
|
5734
6178
|
functionResponse: {
|
|
5735
|
-
name:
|
|
6179
|
+
name: resolvedName,
|
|
5736
6180
|
response: isRecord(message.data) ? message.data : {
|
|
5737
6181
|
content: message.content,
|
|
5738
6182
|
...message.isError !== void 0 ? { isError: message.isError } : {}
|
|
@@ -5779,21 +6223,11 @@ async function geminiRequest(messages, attachments, uploader) {
|
|
|
5779
6223
|
);
|
|
5780
6224
|
const attachmentParts = await geminiAttachmentParts(attachments, uploader);
|
|
5781
6225
|
if (attachmentParts.length > 0) {
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
}
|
|
5788
|
-
}
|
|
5789
|
-
if (targetIndex === -1) {
|
|
5790
|
-
contents.push({
|
|
5791
|
-
role: "user",
|
|
5792
|
-
parts: []
|
|
5793
|
-
});
|
|
5794
|
-
targetIndex = contents.length - 1;
|
|
5795
|
-
}
|
|
5796
|
-
contents[targetIndex].parts.push(...attachmentParts);
|
|
6226
|
+
const target = attachPartsToLastUserTurn(contents, () => ({
|
|
6227
|
+
role: "user",
|
|
6228
|
+
parts: []
|
|
6229
|
+
}));
|
|
6230
|
+
target.parts.push(...attachmentParts);
|
|
5797
6231
|
}
|
|
5798
6232
|
return {
|
|
5799
6233
|
...system ? {
|
|
@@ -5843,6 +6277,22 @@ function extractThoughts(value) {
|
|
|
5843
6277
|
const joined = thoughts.join("\n").trim();
|
|
5844
6278
|
return joined.length > 0 ? joined : void 0;
|
|
5845
6279
|
}
|
|
6280
|
+
function extractThinking(value) {
|
|
6281
|
+
if (!Array.isArray(value)) {
|
|
6282
|
+
return void 0;
|
|
6283
|
+
}
|
|
6284
|
+
const thinking = value.flatMap((part) => {
|
|
6285
|
+
if (part && typeof part === "object" && part.type === "thinking") {
|
|
6286
|
+
const candidate = part.thinking;
|
|
6287
|
+
if (typeof candidate === "string") {
|
|
6288
|
+
return [candidate];
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
return [];
|
|
6292
|
+
});
|
|
6293
|
+
const joined = thinking.join("\n").trim();
|
|
6294
|
+
return joined.length > 0 ? joined : void 0;
|
|
6295
|
+
}
|
|
5846
6296
|
function imageAttachmentsFromPayload(payload, ...sources) {
|
|
5847
6297
|
return extractImagePayloads(payload, ...sources).map(
|
|
5848
6298
|
(item) => preparePortableFile(item)
|
|
@@ -5930,6 +6380,10 @@ function buildOpenAiChatBody(context, messages, parameters, responseFormat, opts
|
|
|
5930
6380
|
model: context.route.model,
|
|
5931
6381
|
messages,
|
|
5932
6382
|
stream: opts.stream,
|
|
6383
|
+
// Ask OpenAI-compatible streaming routes for the trailing usage chunk; without
|
|
6384
|
+
// `stream_options.include_usage` the SSE stream carries no `usage` block and
|
|
6385
|
+
// `runOpenAiStream` would always report `usage: undefined` (H5).
|
|
6386
|
+
...opts.stream ? { stream_options: { include_usage: true } } : {},
|
|
5933
6387
|
...parameters?.maxTokens !== void 0 ? { max_tokens: parameters.maxTokens } : {},
|
|
5934
6388
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
5935
6389
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6084,6 +6538,11 @@ async function* runOpenAiStream(fetchImpl, context) {
|
|
|
6084
6538
|
}
|
|
6085
6539
|
const response = outcome.response;
|
|
6086
6540
|
const streamWarnings = [...uploaderWarnings];
|
|
6541
|
+
if (parameters?.candidateCount && parameters.candidateCount > 1) {
|
|
6542
|
+
streamWarnings.push(
|
|
6543
|
+
"candidateCount > 1 is forwarded to OpenAI-compatible text routes, but the normalized result returns only the first choice."
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6087
6546
|
if (droppedResponseFormat) {
|
|
6088
6547
|
streamWarnings.push(
|
|
6089
6548
|
"response_format was rejected by the OpenAI-compatible route (HTTP 400); the request was retried once without response_format and structured-output formatting was NOT enforced."
|
|
@@ -6229,8 +6688,7 @@ function buildOpenAiStreamResult(context, text, toolCalls, usage, warnings) {
|
|
|
6229
6688
|
} : {}
|
|
6230
6689
|
};
|
|
6231
6690
|
}
|
|
6232
|
-
async function
|
|
6233
|
-
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6691
|
+
async function runProviderImage(fetchImpl, context, opts) {
|
|
6234
6692
|
const referenceImages = imageReferenceFiles(context);
|
|
6235
6693
|
const bundle = buildImagePromptBundle({
|
|
6236
6694
|
request: context.request,
|
|
@@ -6239,10 +6697,10 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6239
6697
|
});
|
|
6240
6698
|
const baseFields = buildImageRequestFields(context, bundle);
|
|
6241
6699
|
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6242
|
-
const endpoint = useEditEndpoint ?
|
|
6700
|
+
const endpoint = useEditEndpoint ? opts.resolveEditEndpoint(context.route) : opts.resolveEndpoint(context.route);
|
|
6243
6701
|
transportSummary(
|
|
6244
6702
|
context,
|
|
6245
|
-
useEditEndpoint ?
|
|
6703
|
+
useEditEndpoint ? `${opts.providerLabel}-image-edit` : `${opts.providerLabel}-image`,
|
|
6246
6704
|
endpoint,
|
|
6247
6705
|
"POST",
|
|
6248
6706
|
false,
|
|
@@ -6255,23 +6713,21 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6255
6713
|
);
|
|
6256
6714
|
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6257
6715
|
method: "POST",
|
|
6258
|
-
headers: {
|
|
6259
|
-
authorization: `Bearer ${apiKey}`
|
|
6260
|
-
},
|
|
6716
|
+
headers: { ...opts.authHeaders },
|
|
6261
6717
|
body: await buildImageEditFormData(context, baseFields),
|
|
6262
6718
|
signal: context.abort.signal
|
|
6263
6719
|
} : {
|
|
6264
6720
|
method: "POST",
|
|
6265
6721
|
headers: {
|
|
6266
6722
|
"content-type": "application/json",
|
|
6267
|
-
|
|
6723
|
+
...opts.authHeaders
|
|
6268
6724
|
},
|
|
6269
6725
|
body: JSON.stringify(baseFields),
|
|
6270
6726
|
signal: context.abort.signal
|
|
6271
6727
|
});
|
|
6272
6728
|
const payload = await readPayload(response);
|
|
6273
6729
|
if (!response.ok) {
|
|
6274
|
-
throw classifyApiError(
|
|
6730
|
+
throw classifyApiError(opts.classifyProvider, response, payload);
|
|
6275
6731
|
}
|
|
6276
6732
|
return {
|
|
6277
6733
|
data: {
|
|
@@ -6281,6 +6737,16 @@ async function runOpenAiImage(fetchImpl, context) {
|
|
|
6281
6737
|
attachments: imageAttachmentsFromPayload(payload)
|
|
6282
6738
|
};
|
|
6283
6739
|
}
|
|
6740
|
+
async function runOpenAiImage(fetchImpl, context) {
|
|
6741
|
+
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6742
|
+
return runProviderImage(fetchImpl, context, {
|
|
6743
|
+
resolveEndpoint: (route) => resolveOpenAiEndpoint(route, "image"),
|
|
6744
|
+
resolveEditEndpoint: (route) => resolveOpenAiImageEditEndpoint(route),
|
|
6745
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6746
|
+
providerLabel: "openai",
|
|
6747
|
+
classifyProvider: "openai"
|
|
6748
|
+
});
|
|
6749
|
+
}
|
|
6284
6750
|
async function runAnthropic(fetchImpl, context) {
|
|
6285
6751
|
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6286
6752
|
const uploaderWarnings = [];
|
|
@@ -6292,6 +6758,8 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6292
6758
|
const parameters = context.request.parameters;
|
|
6293
6759
|
const requestBody = {
|
|
6294
6760
|
model: context.route.model,
|
|
6761
|
+
// Anthropic REQUIRES `max_tokens` (unlike OpenAI/Gemini, where it is optional), so a
|
|
6762
|
+
// provider-specific default of 4096 is applied when the caller omits `maxTokens`.
|
|
6295
6763
|
max_tokens: parameters?.maxTokens ?? 4096,
|
|
6296
6764
|
...parameters?.temperature !== void 0 ? { temperature: parameters.temperature } : {},
|
|
6297
6765
|
...parameters?.topP !== void 0 ? { top_p: parameters.topP } : {},
|
|
@@ -6321,8 +6789,12 @@ async function runAnthropic(fetchImpl, context) {
|
|
|
6321
6789
|
throw classifyApiError("anthropic", response, payload);
|
|
6322
6790
|
}
|
|
6323
6791
|
const data = payload;
|
|
6792
|
+
const anthropicReasoning = extractThinking(data.content);
|
|
6324
6793
|
return {
|
|
6325
6794
|
text: extractText(data.content),
|
|
6795
|
+
// Anthropic extended-thinking blocks, separated from the answer (consumers route this
|
|
6796
|
+
// to a thinking UI), mirroring the Gemini `reasoning` path.
|
|
6797
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {},
|
|
6326
6798
|
data,
|
|
6327
6799
|
...anthropicToolCallsFromPayload(data).length > 0 ? { toolCalls: anthropicToolCallsFromPayload(data) } : {},
|
|
6328
6800
|
...uploaderWarnings.length > 0 ? { warnings: uploaderWarnings } : {},
|
|
@@ -6410,63 +6882,21 @@ async function runGemini(fetchImpl, context) {
|
|
|
6410
6882
|
calls: 1,
|
|
6411
6883
|
...data.usageMetadata?.promptTokenCount !== void 0 ? { inputTokens: data.usageMetadata.promptTokenCount } : {},
|
|
6412
6884
|
...data.usageMetadata?.candidatesTokenCount !== void 0 ? { outputTokens: data.usageMetadata.candidatesTokenCount } : {},
|
|
6413
|
-
...data.usageMetadata?.thoughtsTokenCount !== void 0 ? { reasoningTokens: data.usageMetadata.thoughtsTokenCount } : {}
|
|
6414
|
-
}
|
|
6415
|
-
} : {}
|
|
6416
|
-
};
|
|
6417
|
-
}
|
|
6418
|
-
async function runGeminiImage(fetchImpl, context) {
|
|
6419
|
-
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6420
|
-
const referenceImages = imageReferenceFiles(context);
|
|
6421
|
-
const bundle = buildImagePromptBundle({
|
|
6422
|
-
request: context.request,
|
|
6423
|
-
model: context.route.model,
|
|
6424
|
-
referenceImages
|
|
6425
|
-
});
|
|
6426
|
-
const baseFields = buildImageRequestFields(context, bundle);
|
|
6427
|
-
const useEditEndpoint = shouldUseImageEditEndpoint(context);
|
|
6428
|
-
const endpoint = useEditEndpoint ? resolveGeminiImageEditEndpoint(context.route) : resolveGeminiImageEndpoint(context.route);
|
|
6429
|
-
transportSummary(
|
|
6430
|
-
context,
|
|
6431
|
-
useEditEndpoint ? "gemini-image-edit" : "gemini-image",
|
|
6432
|
-
endpoint,
|
|
6433
|
-
"POST",
|
|
6434
|
-
false,
|
|
6435
|
-
{
|
|
6436
|
-
...baseFields,
|
|
6437
|
-
source_image: Boolean(context.request.image?.sourceImage),
|
|
6438
|
-
mask: Boolean(context.request.image?.mask),
|
|
6439
|
-
reference_image_count: referenceImages.length
|
|
6440
|
-
}
|
|
6441
|
-
);
|
|
6442
|
-
const response = await fetchImpl(endpoint, useEditEndpoint ? {
|
|
6443
|
-
method: "POST",
|
|
6444
|
-
headers: {
|
|
6445
|
-
authorization: `Bearer ${apiKey}`
|
|
6446
|
-
},
|
|
6447
|
-
body: await buildImageEditFormData(context, baseFields),
|
|
6448
|
-
signal: context.abort.signal
|
|
6449
|
-
} : {
|
|
6450
|
-
method: "POST",
|
|
6451
|
-
headers: {
|
|
6452
|
-
"content-type": "application/json",
|
|
6453
|
-
authorization: `Bearer ${apiKey}`
|
|
6454
|
-
},
|
|
6455
|
-
body: JSON.stringify(baseFields),
|
|
6456
|
-
signal: context.abort.signal
|
|
6457
|
-
});
|
|
6458
|
-
const payload = await readPayload(response);
|
|
6459
|
-
if (!response.ok) {
|
|
6460
|
-
throw classifyApiError(context.route.provider, response, payload);
|
|
6461
|
-
}
|
|
6462
|
-
return {
|
|
6463
|
-
data: {
|
|
6464
|
-
request: bundle,
|
|
6465
|
-
response: payload
|
|
6466
|
-
},
|
|
6467
|
-
attachments: imageAttachmentsFromPayload(payload)
|
|
6885
|
+
...data.usageMetadata?.thoughtsTokenCount !== void 0 ? { reasoningTokens: data.usageMetadata.thoughtsTokenCount } : {}
|
|
6886
|
+
}
|
|
6887
|
+
} : {}
|
|
6468
6888
|
};
|
|
6469
6889
|
}
|
|
6890
|
+
async function runGeminiImage(fetchImpl, context) {
|
|
6891
|
+
const apiKey = resolveApiKey(context.route, context.runtime);
|
|
6892
|
+
return runProviderImage(fetchImpl, context, {
|
|
6893
|
+
resolveEndpoint: (route) => resolveGeminiImageEndpoint(route),
|
|
6894
|
+
resolveEditEndpoint: (route) => resolveGeminiImageEditEndpoint(route),
|
|
6895
|
+
authHeaders: { authorization: `Bearer ${apiKey}` },
|
|
6896
|
+
providerLabel: "gemini",
|
|
6897
|
+
classifyProvider: context.route.provider
|
|
6898
|
+
});
|
|
6899
|
+
}
|
|
6470
6900
|
function verifyApiRoute(route, runtime) {
|
|
6471
6901
|
resolveApiKey(route, runtime);
|
|
6472
6902
|
return [];
|
|
@@ -6544,9 +6974,12 @@ var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
|
|
|
6544
6974
|
transportId: "opencode-server"
|
|
6545
6975
|
}
|
|
6546
6976
|
};
|
|
6547
|
-
var AI_CONNECT_DEFAULT_SERVER_COMMANDS =
|
|
6548
|
-
|
|
6549
|
-
}
|
|
6977
|
+
var AI_CONNECT_DEFAULT_SERVER_COMMANDS = Object.fromEntries(
|
|
6978
|
+
Object.values(AI_CONNECT_DEFAULT_SERVER_PRESETS).map((preset) => [
|
|
6979
|
+
`${preset.id}:${preset.transportId}`,
|
|
6980
|
+
preset.command
|
|
6981
|
+
])
|
|
6982
|
+
);
|
|
6550
6983
|
|
|
6551
6984
|
// src/catalog.ts
|
|
6552
6985
|
var TEXT_PROVIDER_CATALOG = [
|
|
@@ -6763,8 +7196,119 @@ import fs from "node:fs/promises";
|
|
|
6763
7196
|
import os from "node:os";
|
|
6764
7197
|
import path from "node:path";
|
|
6765
7198
|
import { pathToFileURL } from "node:url";
|
|
7199
|
+
|
|
7200
|
+
// src/transport-runtime.ts
|
|
7201
|
+
var MINIMAL_ENV_KEYS = [
|
|
7202
|
+
"PATH",
|
|
7203
|
+
"HOME",
|
|
7204
|
+
"USERPROFILE",
|
|
7205
|
+
"HTTPS_PROXY",
|
|
7206
|
+
"HTTP_PROXY",
|
|
7207
|
+
"NO_PROXY",
|
|
7208
|
+
"https_proxy",
|
|
7209
|
+
"http_proxy",
|
|
7210
|
+
"no_proxy",
|
|
7211
|
+
"NODE_EXTRA_CA_CERTS",
|
|
7212
|
+
"SSL_CERT_FILE",
|
|
7213
|
+
"SSL_CERT_DIR",
|
|
7214
|
+
"SystemRoot",
|
|
7215
|
+
"windir",
|
|
7216
|
+
"APPDATA",
|
|
7217
|
+
"LOCALAPPDATA",
|
|
7218
|
+
"TEMP",
|
|
7219
|
+
"TMP",
|
|
7220
|
+
"PATHEXT",
|
|
7221
|
+
"ComSpec",
|
|
7222
|
+
"NUMBER_OF_PROCESSORS"
|
|
7223
|
+
];
|
|
7224
|
+
function resolveChildEnv(opts = {}) {
|
|
7225
|
+
const extra = opts.extra ?? {};
|
|
7226
|
+
if (opts.envMode === "inherit") {
|
|
7227
|
+
const inherited = {};
|
|
7228
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
7229
|
+
if (typeof value === "string") {
|
|
7230
|
+
inherited[key] = value;
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
return { ...inherited, ...extra };
|
|
7234
|
+
}
|
|
7235
|
+
const minimal = {};
|
|
7236
|
+
const keys = /* @__PURE__ */ new Set([...MINIMAL_ENV_KEYS, ...opts.allowlist ?? []]);
|
|
7237
|
+
for (const key of keys) {
|
|
7238
|
+
const value = process.env[key];
|
|
7239
|
+
if (typeof value === "string") {
|
|
7240
|
+
minimal[key] = value;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
return { ...minimal, ...extra };
|
|
7244
|
+
}
|
|
7245
|
+
function killProcessTree(child, signal, detached) {
|
|
7246
|
+
if (process.platform !== "win32" && detached && typeof child.pid === "number") {
|
|
7247
|
+
try {
|
|
7248
|
+
process.kill(-child.pid, signal);
|
|
7249
|
+
return;
|
|
7250
|
+
} catch {
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
try {
|
|
7254
|
+
child.kill(signal);
|
|
7255
|
+
} catch {
|
|
7256
|
+
}
|
|
7257
|
+
}
|
|
7258
|
+
function resolveManagedCommand(key, optionsCommands, routeCommand, presetCommand) {
|
|
7259
|
+
const resolved = optionsCommands?.[key]?.trim() || routeCommand?.trim() || presetCommand?.trim();
|
|
7260
|
+
if (!resolved) {
|
|
7261
|
+
throw new AiConnectError(
|
|
7262
|
+
"validation_error",
|
|
7263
|
+
`No managed command is configured for "${key}".`
|
|
7264
|
+
);
|
|
7265
|
+
}
|
|
7266
|
+
return resolved;
|
|
7267
|
+
}
|
|
7268
|
+
var trackedChildren = /* @__PURE__ */ new Set();
|
|
7269
|
+
var exitHookRegistered = false;
|
|
7270
|
+
function killAllTrackedChildren() {
|
|
7271
|
+
for (const tracked of trackedChildren) {
|
|
7272
|
+
killProcessTree(tracked.child, "SIGKILL", tracked.detached);
|
|
7273
|
+
}
|
|
7274
|
+
trackedChildren.clear();
|
|
7275
|
+
}
|
|
7276
|
+
function ensureExitHook() {
|
|
7277
|
+
if (exitHookRegistered) {
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
exitHookRegistered = true;
|
|
7281
|
+
process.on("exit", () => {
|
|
7282
|
+
killAllTrackedChildren();
|
|
7283
|
+
});
|
|
7284
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
7285
|
+
const handler = () => {
|
|
7286
|
+
killAllTrackedChildren();
|
|
7287
|
+
process.removeListener(signal, handler);
|
|
7288
|
+
if (process.listenerCount(signal) === 0) {
|
|
7289
|
+
process.kill(process.pid, signal);
|
|
7290
|
+
}
|
|
7291
|
+
};
|
|
7292
|
+
process.on(signal, handler);
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
function trackChild(child, detached) {
|
|
7296
|
+
ensureExitHook();
|
|
7297
|
+
const entry = { child, detached };
|
|
7298
|
+
trackedChildren.add(entry);
|
|
7299
|
+
const untrack = () => {
|
|
7300
|
+
trackedChildren.delete(entry);
|
|
7301
|
+
};
|
|
7302
|
+
child.once("close", untrack);
|
|
7303
|
+
child.once("exit", untrack);
|
|
7304
|
+
return untrack;
|
|
7305
|
+
}
|
|
7306
|
+
|
|
7307
|
+
// src/acp.ts
|
|
6766
7308
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
6767
7309
|
var DEFAULT_SESSION_CACHE_IDLE_TTL_MS = 6e4;
|
|
7310
|
+
var MAX_ACP_STDERR_CHARS = 64 * 1024;
|
|
7311
|
+
var MAX_ACP_STREAM_QUEUE = 1024;
|
|
6768
7312
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
6769
7313
|
[".png", "image/png"],
|
|
6770
7314
|
[".jpg", "image/jpeg"],
|
|
@@ -6797,6 +7341,20 @@ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
6797
7341
|
function isRecord2(value) {
|
|
6798
7342
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6799
7343
|
}
|
|
7344
|
+
var ACP_HARNESS_NOISE_PATTERNS = [
|
|
7345
|
+
/^модель переключена на .+?\.?(?:\s*готов к работе[.!…]*)?$/iu,
|
|
7346
|
+
/^готов к работе[.!…]*$/iu,
|
|
7347
|
+
/^жду (?:вашего вопроса|вопроса или задачи|вашей задачи|вашего ответа|явного запроса)[^]*$/iu,
|
|
7348
|
+
/^это сообщение помечено как локальная команда[^]*$/iu,
|
|
7349
|
+
/^<local-command-(?:caveat|stdout|name|message|args)[^>]*>[^]*$/iu
|
|
7350
|
+
];
|
|
7351
|
+
function isHarnessNoiseLine(line) {
|
|
7352
|
+
const trimmed = line.trim();
|
|
7353
|
+
if (trimmed.length === 0) {
|
|
7354
|
+
return false;
|
|
7355
|
+
}
|
|
7356
|
+
return ACP_HARNESS_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
7357
|
+
}
|
|
6800
7358
|
function splitCommandLine(commandLine) {
|
|
6801
7359
|
const matches = commandLine.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? [];
|
|
6802
7360
|
const parts = matches.map((part) => {
|
|
@@ -7039,24 +7597,29 @@ function prepareClaudeLaunchRuntime(base, launch) {
|
|
|
7039
7597
|
launch
|
|
7040
7598
|
};
|
|
7041
7599
|
}
|
|
7600
|
+
var ACP_LAUNCH_PREPARERS = {
|
|
7601
|
+
"opencode-acp": prepareOpenCodeLaunchRuntime,
|
|
7602
|
+
"codex-acp": prepareCodexLaunchRuntime,
|
|
7603
|
+
"claude-code-acp": prepareClaudeLaunchRuntime
|
|
7604
|
+
};
|
|
7042
7605
|
async function prepareAcpLaunchRuntime(route, options, commandLine, cwdOverride) {
|
|
7043
7606
|
const launch = normalizeLaunchOptions(route, options);
|
|
7044
7607
|
const base = {
|
|
7045
7608
|
commandLine,
|
|
7046
7609
|
cwd: path.resolve(cwdOverride ?? options?.cwd ?? process.cwd()),
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
}
|
|
7610
|
+
// SYS-2: minimal env by default — do NOT hand the host's full process.env
|
|
7611
|
+
// (other-provider keys, CI tokens) to third-party ACP agents. Extra env is
|
|
7612
|
+
// layered via options.env.
|
|
7613
|
+
env: resolveChildEnv({ extra: options?.env ?? {} })
|
|
7051
7614
|
};
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
if (route.transport.id === "codex-acp") {
|
|
7056
|
-
return prepareCodexLaunchRuntime(base, launch);
|
|
7615
|
+
const preparer = ACP_LAUNCH_PREPARERS[route.transport.id];
|
|
7616
|
+
if (preparer) {
|
|
7617
|
+
return await preparer(base, launch);
|
|
7057
7618
|
}
|
|
7058
|
-
if (
|
|
7059
|
-
|
|
7619
|
+
if (launch.contextMode !== "workspace" || launch.skillsMode !== "default") {
|
|
7620
|
+
console.warn(
|
|
7621
|
+
`[ai-connect] ACP transport "${route.transport.id}" has no launch-runtime preparer; requested contextMode/skillsMode isolation is not applied.`
|
|
7622
|
+
);
|
|
7060
7623
|
}
|
|
7061
7624
|
return {
|
|
7062
7625
|
...base,
|
|
@@ -7088,8 +7651,27 @@ function mimeTypeFromPath(filePath) {
|
|
|
7088
7651
|
function isTextPath(filePath) {
|
|
7089
7652
|
return TEXT_EXTENSIONS2.has(path.extname(filePath).toLowerCase());
|
|
7090
7653
|
}
|
|
7091
|
-
function
|
|
7092
|
-
|
|
7654
|
+
async function resolveRealPathTolerant(target) {
|
|
7655
|
+
try {
|
|
7656
|
+
return await fs.realpath(target);
|
|
7657
|
+
} catch {
|
|
7658
|
+
const parent = path.dirname(target);
|
|
7659
|
+
if (parent === target) {
|
|
7660
|
+
return target;
|
|
7661
|
+
}
|
|
7662
|
+
const realParent = await resolveRealPathTolerant(parent);
|
|
7663
|
+
return path.join(realParent, path.basename(target));
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
async function isWithinRoot(rootDir, targetPath) {
|
|
7667
|
+
let realRoot;
|
|
7668
|
+
try {
|
|
7669
|
+
realRoot = await fs.realpath(rootDir);
|
|
7670
|
+
} catch {
|
|
7671
|
+
realRoot = path.resolve(rootDir);
|
|
7672
|
+
}
|
|
7673
|
+
const realTarget = await resolveRealPathTolerant(path.resolve(targetPath));
|
|
7674
|
+
const relative = path.relative(realRoot, realTarget);
|
|
7093
7675
|
return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
7094
7676
|
}
|
|
7095
7677
|
function buildMessageTranscript(context) {
|
|
@@ -7516,46 +8098,58 @@ function extractTextChunk(update) {
|
|
|
7516
8098
|
}
|
|
7517
8099
|
return typeof content.text === "string" && content.text.length > 0 ? content.text : void 0;
|
|
7518
8100
|
}
|
|
8101
|
+
function isAuthErrorMessage(message) {
|
|
8102
|
+
return /unauthor|authentication|invalid[_\s-]?(api[_\s-]?key|credential|token)|forbidden|\b40[13]\b/i.test(
|
|
8103
|
+
message
|
|
8104
|
+
);
|
|
8105
|
+
}
|
|
7519
8106
|
function normalizeAcpError(error) {
|
|
7520
8107
|
if (error instanceof AiConnectError) {
|
|
7521
8108
|
return error;
|
|
7522
8109
|
}
|
|
8110
|
+
if (error instanceof Error) {
|
|
8111
|
+
const message = error.message || "ACP transport failed.";
|
|
8112
|
+
const errno = error.code;
|
|
8113
|
+
if (errno === "ENOENT" || errno === "EACCES" || errno === "ENOEXEC" || message.toLowerCase().includes("timed out")) {
|
|
8114
|
+
return new AiConnectError("local_harness_unavailable", message);
|
|
8115
|
+
}
|
|
8116
|
+
if (isAuthErrorMessage(message)) {
|
|
8117
|
+
return new AiConnectError("auth_error", message);
|
|
8118
|
+
}
|
|
8119
|
+
return new AiConnectError("temporary_unavailable", message);
|
|
8120
|
+
}
|
|
7523
8121
|
if (isRecord2(error)) {
|
|
7524
8122
|
const code = typeof error.code === "number" ? error.code : void 0;
|
|
7525
8123
|
const message = typeof error.message === "string" ? error.message : "ACP transport failed.";
|
|
7526
8124
|
if (code === -32601 || code === -32602) {
|
|
7527
8125
|
return new AiConnectError("not_supported", message);
|
|
7528
8126
|
}
|
|
7529
|
-
if (message
|
|
8127
|
+
if (isAuthErrorMessage(message)) {
|
|
7530
8128
|
return new AiConnectError("auth_error", message);
|
|
7531
8129
|
}
|
|
7532
8130
|
return new AiConnectError("temporary_unavailable", message);
|
|
7533
8131
|
}
|
|
7534
|
-
if (error instanceof Error) {
|
|
7535
|
-
const message = error.message;
|
|
7536
|
-
if ("code" in error && error.code === "ENOENT") {
|
|
7537
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7538
|
-
}
|
|
7539
|
-
if (message.toLowerCase().includes("timed out")) {
|
|
7540
|
-
return new AiConnectError("local_harness_unavailable", message);
|
|
7541
|
-
}
|
|
7542
|
-
return new AiConnectError("temporary_unavailable", message);
|
|
7543
|
-
}
|
|
7544
8132
|
return new AiConnectError(
|
|
7545
8133
|
"temporary_unavailable",
|
|
7546
8134
|
"ACP transport failed."
|
|
7547
8135
|
);
|
|
7548
8136
|
}
|
|
7549
8137
|
function resolveAcpCommand(route, options) {
|
|
7550
|
-
const
|
|
7551
|
-
const
|
|
7552
|
-
|
|
8138
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
8139
|
+
const optionsOverride = options?.commands?.[key] ?? options?.commands?.[route.transport.id] ?? options?.commands?.[route.provider];
|
|
8140
|
+
try {
|
|
8141
|
+
return resolveManagedCommand(
|
|
8142
|
+
key,
|
|
8143
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
8144
|
+
route.transport.command,
|
|
8145
|
+
AI_CONNECT_DEFAULT_ACP_COMMANDS[key]
|
|
8146
|
+
);
|
|
8147
|
+
} catch {
|
|
7553
8148
|
throw new AiConnectError(
|
|
7554
8149
|
"validation_error",
|
|
7555
8150
|
`No ACP command is configured for route "${route.id}".`
|
|
7556
8151
|
);
|
|
7557
8152
|
}
|
|
7558
|
-
return resolved;
|
|
7559
8153
|
}
|
|
7560
8154
|
function resolveAcpExecutable(route, options) {
|
|
7561
8155
|
return splitCommandLine(resolveAcpCommand(route, options)).command;
|
|
@@ -7633,6 +8227,7 @@ function buildAcpLifecycle(route, authRequest, mode) {
|
|
|
7633
8227
|
}
|
|
7634
8228
|
steps.push("session/new");
|
|
7635
8229
|
if (mode === "prompt") {
|
|
8230
|
+
steps.push("session/set_model");
|
|
7636
8231
|
steps.push("session/prompt");
|
|
7637
8232
|
}
|
|
7638
8233
|
return { steps, keys };
|
|
@@ -7705,14 +8300,7 @@ async function settleClosePromise(closePromise, timeoutMs = 1500) {
|
|
|
7705
8300
|
]);
|
|
7706
8301
|
}
|
|
7707
8302
|
function killAcpProcess(child, signal) {
|
|
7708
|
-
|
|
7709
|
-
try {
|
|
7710
|
-
process.kill(-child.pid, signal);
|
|
7711
|
-
return;
|
|
7712
|
-
} catch {
|
|
7713
|
-
}
|
|
7714
|
-
}
|
|
7715
|
-
child.kill(signal);
|
|
8303
|
+
killProcessTree(child, signal, process.platform !== "win32");
|
|
7716
8304
|
}
|
|
7717
8305
|
var AcpConnection = class {
|
|
7718
8306
|
commandLine;
|
|
@@ -7720,6 +8308,9 @@ var AcpConnection = class {
|
|
|
7720
8308
|
env;
|
|
7721
8309
|
timeoutMs;
|
|
7722
8310
|
permissionMode;
|
|
8311
|
+
selectModel;
|
|
8312
|
+
suppressHarnessNoise;
|
|
8313
|
+
failOnHarnessOnlyTurn;
|
|
7723
8314
|
command;
|
|
7724
8315
|
args;
|
|
7725
8316
|
child;
|
|
@@ -7741,6 +8332,9 @@ var AcpConnection = class {
|
|
|
7741
8332
|
this.env = options.env;
|
|
7742
8333
|
this.timeoutMs = options.timeoutMs;
|
|
7743
8334
|
this.permissionMode = options.permissionMode;
|
|
8335
|
+
this.selectModel = options.selectModel;
|
|
8336
|
+
this.suppressHarnessNoise = options.suppressHarnessNoise;
|
|
8337
|
+
this.failOnHarnessOnlyTurn = options.failOnHarnessOnlyTurn;
|
|
7744
8338
|
const resolved = splitCommandLine(options.commandLine);
|
|
7745
8339
|
this.command = resolved.command;
|
|
7746
8340
|
this.args = resolved.args;
|
|
@@ -7773,8 +8367,18 @@ var AcpConnection = class {
|
|
|
7773
8367
|
usage: void 0,
|
|
7774
8368
|
attachments: [],
|
|
7775
8369
|
warnings: [],
|
|
8370
|
+
suppressNoise: this.suppressHarnessNoise,
|
|
8371
|
+
lineBuffer: "",
|
|
8372
|
+
harnessMarkers: [],
|
|
7776
8373
|
...onDelta ? { onDelta } : {}
|
|
7777
8374
|
};
|
|
8375
|
+
await this.selectSessionModel(
|
|
8376
|
+
sessionId,
|
|
8377
|
+
context.route,
|
|
8378
|
+
session,
|
|
8379
|
+
transport.phases ?? [],
|
|
8380
|
+
this.activePrompt.warnings
|
|
8381
|
+
);
|
|
7778
8382
|
let stopReason;
|
|
7779
8383
|
const abortSignal = context.abort.signal;
|
|
7780
8384
|
let abortListener;
|
|
@@ -7817,13 +8421,30 @@ var AcpConnection = class {
|
|
|
7817
8421
|
}
|
|
7818
8422
|
this.bumpIdleTimer();
|
|
7819
8423
|
}
|
|
8424
|
+
this.flushHarnessLineBuffer();
|
|
7820
8425
|
const stderrSlice = this.stderr.slice(stderrOffset).trim();
|
|
7821
8426
|
const warnings = [...this.activePrompt.warnings];
|
|
8427
|
+
if (this.activePrompt.harnessMarkers.length > 0) {
|
|
8428
|
+
warnings.push(
|
|
8429
|
+
`ACP: filtered ${this.activePrompt.harnessMarkers.length} interactive-harness line(s) from the response.`
|
|
8430
|
+
);
|
|
8431
|
+
}
|
|
7822
8432
|
if (stderrSlice.length > 0) {
|
|
7823
8433
|
warnings.push(stderrSlice);
|
|
7824
8434
|
}
|
|
8435
|
+
if (this.stderr.length > MAX_ACP_STDERR_CHARS) {
|
|
8436
|
+
this.stderr = this.stderr.slice(-MAX_ACP_STDERR_CHARS);
|
|
8437
|
+
}
|
|
8438
|
+
const text = this.activePrompt.text.trim();
|
|
8439
|
+
if (this.failOnHarnessOnlyTurn && this.activePrompt.harnessMarkers.length > 0 && text.length === 0) {
|
|
8440
|
+
this.activePrompt = void 0;
|
|
8441
|
+
throw new AiConnectError(
|
|
8442
|
+
"temporary_unavailable",
|
|
8443
|
+
"ACP turn returned only interactive-harness output (no task output)."
|
|
8444
|
+
);
|
|
8445
|
+
}
|
|
7825
8446
|
const result = {
|
|
7826
|
-
text
|
|
8447
|
+
text,
|
|
7827
8448
|
attachments: [...this.activePrompt.attachments],
|
|
7828
8449
|
warnings,
|
|
7829
8450
|
...this.activePrompt.usage ? { usage: this.activePrompt.usage } : {},
|
|
@@ -7983,6 +8604,12 @@ var AcpConnection = class {
|
|
|
7983
8604
|
this.stderr = "";
|
|
7984
8605
|
this.pending.clear();
|
|
7985
8606
|
this.messageQueue = Promise.resolve();
|
|
8607
|
+
child.stdin?.on("error", (error) => {
|
|
8608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8609
|
+
this.stderr += `[ai-connect] ACP stdin error: ${message}
|
|
8610
|
+
`;
|
|
8611
|
+
});
|
|
8612
|
+
trackChild(child, process.platform !== "win32");
|
|
7986
8613
|
child.stdout.setEncoding("utf8");
|
|
7987
8614
|
child.stdout.on("data", (chunk) => {
|
|
7988
8615
|
this.buffer += chunk;
|
|
@@ -8046,8 +8673,14 @@ var AcpConnection = class {
|
|
|
8046
8673
|
if (!stdin) {
|
|
8047
8674
|
return;
|
|
8048
8675
|
}
|
|
8049
|
-
|
|
8676
|
+
try {
|
|
8677
|
+
stdin.write(`${JSON.stringify(message)}
|
|
8050
8678
|
`);
|
|
8679
|
+
} catch (error) {
|
|
8680
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
8681
|
+
this.stderr += `[ai-connect] ACP stdin write error (reply): ${messageText}
|
|
8682
|
+
`;
|
|
8683
|
+
}
|
|
8051
8684
|
}
|
|
8052
8685
|
failPending(error) {
|
|
8053
8686
|
for (const entry of this.pending.values()) {
|
|
@@ -8068,7 +8701,7 @@ var AcpConnection = class {
|
|
|
8068
8701
|
}
|
|
8069
8702
|
if (message.method === "readTextFile") {
|
|
8070
8703
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
8071
|
-
if (!filePath || !isWithinRoot(this.cwd, filePath)) {
|
|
8704
|
+
if (!filePath || !await isWithinRoot(this.cwd, filePath)) {
|
|
8072
8705
|
this.reply({
|
|
8073
8706
|
jsonrpc: "2.0",
|
|
8074
8707
|
id: message.id,
|
|
@@ -8101,7 +8734,7 @@ var AcpConnection = class {
|
|
|
8101
8734
|
}
|
|
8102
8735
|
const filePath = typeof params.path === "string" ? path.resolve(params.path) : void 0;
|
|
8103
8736
|
const content = typeof params.content === "string" ? params.content : void 0;
|
|
8104
|
-
if (!filePath || content === void 0 || !isWithinRoot(this.cwd, filePath)) {
|
|
8737
|
+
if (!filePath || content === void 0 || !await isWithinRoot(this.cwd, filePath)) {
|
|
8105
8738
|
this.reply({
|
|
8106
8739
|
jsonrpc: "2.0",
|
|
8107
8740
|
id: message.id,
|
|
@@ -8130,6 +8763,108 @@ var AcpConnection = class {
|
|
|
8130
8763
|
}
|
|
8131
8764
|
});
|
|
8132
8765
|
}
|
|
8766
|
+
/**
|
|
8767
|
+
* Select the route's model through the ACP protocol (TASK-033 / UR-002).
|
|
8768
|
+
* Best-effort matrix: skip when disabled / no model / no advertised catalog /
|
|
8769
|
+
* already current; warn (not silently default) when the requested model is not
|
|
8770
|
+
* in the agent's `availableModels`; otherwise `session/set_model` and surface
|
|
8771
|
+
* any error as a warning. Never throws — a failed protocol selection must not
|
|
8772
|
+
* fail the generation.
|
|
8773
|
+
*/
|
|
8774
|
+
async selectSessionModel(sessionId, route, sessionResult, phases, warnings) {
|
|
8775
|
+
if (!this.selectModel) {
|
|
8776
|
+
return;
|
|
8777
|
+
}
|
|
8778
|
+
const desiredModel = route.model?.trim();
|
|
8779
|
+
if (!desiredModel) {
|
|
8780
|
+
return;
|
|
8781
|
+
}
|
|
8782
|
+
const catalog = modelCatalogFromSession(sessionResult);
|
|
8783
|
+
if (!catalog) {
|
|
8784
|
+
return;
|
|
8785
|
+
}
|
|
8786
|
+
if (catalog.currentModelId === desiredModel) {
|
|
8787
|
+
return;
|
|
8788
|
+
}
|
|
8789
|
+
const available = catalog.availableModels ?? [];
|
|
8790
|
+
if (available.length > 0 && !available.some((entry) => entry.modelId === desiredModel)) {
|
|
8791
|
+
warnings.push(
|
|
8792
|
+
`ACP agent's available models [${available.map((entry) => entry.modelId).join(", ")}] do not include route model "${desiredModel}"; model not switched via protocol.`
|
|
8793
|
+
);
|
|
8794
|
+
return;
|
|
8795
|
+
}
|
|
8796
|
+
try {
|
|
8797
|
+
await measurePhase(
|
|
8798
|
+
phases,
|
|
8799
|
+
"session/set_model",
|
|
8800
|
+
async () => this.request("session/set_model", {
|
|
8801
|
+
sessionId,
|
|
8802
|
+
modelId: desiredModel
|
|
8803
|
+
})
|
|
8804
|
+
);
|
|
8805
|
+
} catch (error) {
|
|
8806
|
+
const message = error instanceof Error ? error.message : isRecord2(error) && typeof error.message === "string" ? error.message : String(error);
|
|
8807
|
+
warnings.push(
|
|
8808
|
+
`ACP session/set_model("${desiredModel}") failed: ${message}`
|
|
8809
|
+
);
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8812
|
+
/**
|
|
8813
|
+
* Append `agent_message_chunk` text (TASK-033 / UR-003). With suppression off
|
|
8814
|
+
* (legacy) the chunk streams + accumulates raw. With suppression on, the chunk
|
|
8815
|
+
* is line-buffered: each COMPLETE line is classified and either dropped (a
|
|
8816
|
+
* harness marker — no delta, no accumulation) or emitted; the trailing partial
|
|
8817
|
+
* line waits in `lineBuffer` for the next chunk or the end-of-turn flush.
|
|
8818
|
+
*/
|
|
8819
|
+
appendAgentText(chunk) {
|
|
8820
|
+
const state = this.activePrompt;
|
|
8821
|
+
if (!state) {
|
|
8822
|
+
return;
|
|
8823
|
+
}
|
|
8824
|
+
if (!state.suppressNoise) {
|
|
8825
|
+
state.onDelta?.(chunk);
|
|
8826
|
+
state.text += chunk;
|
|
8827
|
+
return;
|
|
8828
|
+
}
|
|
8829
|
+
state.lineBuffer += chunk;
|
|
8830
|
+
let newlineIndex = state.lineBuffer.indexOf("\n");
|
|
8831
|
+
while (newlineIndex !== -1) {
|
|
8832
|
+
const line = state.lineBuffer.slice(0, newlineIndex + 1);
|
|
8833
|
+
state.lineBuffer = state.lineBuffer.slice(newlineIndex + 1);
|
|
8834
|
+
this.emitAgentLine(line);
|
|
8835
|
+
newlineIndex = state.lineBuffer.indexOf("\n");
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
/** Flush any trailing partial line through the harness-noise classifier. */
|
|
8839
|
+
flushHarnessLineBuffer() {
|
|
8840
|
+
const state = this.activePrompt;
|
|
8841
|
+
if (!state || !state.suppressNoise) {
|
|
8842
|
+
return;
|
|
8843
|
+
}
|
|
8844
|
+
const remaining = state.lineBuffer;
|
|
8845
|
+
state.lineBuffer = "";
|
|
8846
|
+
if (remaining.length > 0) {
|
|
8847
|
+
this.emitAgentLine(remaining);
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
/**
|
|
8851
|
+
* Emit one classified line: a curated harness marker is recorded + dropped
|
|
8852
|
+
* (no delta, no accumulation); any other line streams + accumulates. `line`
|
|
8853
|
+
* may carry a trailing newline — classification runs on the content.
|
|
8854
|
+
*/
|
|
8855
|
+
emitAgentLine(line) {
|
|
8856
|
+
const state = this.activePrompt;
|
|
8857
|
+
if (!state) {
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
const content = line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
8861
|
+
if (isHarnessNoiseLine(content)) {
|
|
8862
|
+
state.harnessMarkers.push(content.trim());
|
|
8863
|
+
return;
|
|
8864
|
+
}
|
|
8865
|
+
state.onDelta?.(line);
|
|
8866
|
+
state.text += line;
|
|
8867
|
+
}
|
|
8133
8868
|
handleNotification(message) {
|
|
8134
8869
|
if (message.method !== "session/update" || !this.activePrompt) {
|
|
8135
8870
|
return;
|
|
@@ -8144,8 +8879,7 @@ var AcpConnection = class {
|
|
|
8144
8879
|
}
|
|
8145
8880
|
const chunk = extractTextChunk(update);
|
|
8146
8881
|
if (update.sessionUpdate === "agent_message_chunk" && chunk) {
|
|
8147
|
-
this.
|
|
8148
|
-
this.activePrompt.text += chunk;
|
|
8882
|
+
this.appendAgentText(chunk);
|
|
8149
8883
|
}
|
|
8150
8884
|
if (update.sessionUpdate === "agent_thought_chunk" && chunk) {
|
|
8151
8885
|
this.activePrompt.warnings.push(chunk);
|
|
@@ -8274,8 +9008,20 @@ var AcpConnection = class {
|
|
|
8274
9008
|
);
|
|
8275
9009
|
return;
|
|
8276
9010
|
}
|
|
8277
|
-
|
|
9011
|
+
try {
|
|
9012
|
+
stdin.write(`${JSON.stringify(payload)}
|
|
8278
9013
|
`);
|
|
9014
|
+
} catch (error) {
|
|
9015
|
+
clearTimeout(timer);
|
|
9016
|
+
this.pending.delete(id);
|
|
9017
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
9018
|
+
reject(
|
|
9019
|
+
new AiConnectError(
|
|
9020
|
+
"local_harness_unavailable",
|
|
9021
|
+
`ACP command "${this.commandLine}" stdin write failed: ${messageText}`
|
|
9022
|
+
)
|
|
9023
|
+
);
|
|
9024
|
+
}
|
|
8279
9025
|
});
|
|
8280
9026
|
}
|
|
8281
9027
|
};
|
|
@@ -8332,7 +9078,10 @@ function createAcpTransportManager(options) {
|
|
|
8332
9078
|
cwd: runtime.cwd,
|
|
8333
9079
|
env: runtime.env,
|
|
8334
9080
|
timeoutMs: promptTimeout(options),
|
|
8335
|
-
permissionMode: options?.permissionMode ?? "deny-all"
|
|
9081
|
+
permissionMode: options?.permissionMode ?? "deny-all",
|
|
9082
|
+
selectModel: options?.selectModel ?? true,
|
|
9083
|
+
suppressHarnessNoise: options?.suppressHarnessNoise ?? true,
|
|
9084
|
+
failOnHarnessOnlyTurn: options?.failOnHarnessOnlyTurn ?? true
|
|
8336
9085
|
};
|
|
8337
9086
|
}
|
|
8338
9087
|
function getPool(key) {
|
|
@@ -8543,6 +9292,10 @@ function createAcpTransportManager(options) {
|
|
|
8543
9292
|
let notify;
|
|
8544
9293
|
const onDelta = (text) => {
|
|
8545
9294
|
deltas.push(text);
|
|
9295
|
+
if (deltas.length > MAX_ACP_STREAM_QUEUE) {
|
|
9296
|
+
const overflow = deltas.splice(0, deltas.length - MAX_ACP_STREAM_QUEUE);
|
|
9297
|
+
deltas.unshift(overflow.join(""));
|
|
9298
|
+
}
|
|
8546
9299
|
notify?.();
|
|
8547
9300
|
};
|
|
8548
9301
|
let settled = false;
|
|
@@ -8680,44 +9433,29 @@ ${message.content}`;
|
|
|
8680
9433
|
}).join("\n\n");
|
|
8681
9434
|
}
|
|
8682
9435
|
function resolveCliCommand(route, options) {
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
}
|
|
8686
|
-
const candidates = [
|
|
8687
|
-
`${route.provider}:${route.transport.id}`,
|
|
8688
|
-
route.transport.id,
|
|
8689
|
-
route.provider
|
|
8690
|
-
];
|
|
8691
|
-
for (const candidate of candidates) {
|
|
8692
|
-
const override = options?.commands?.[candidate]?.trim();
|
|
8693
|
-
if (override) {
|
|
8694
|
-
return override;
|
|
8695
|
-
}
|
|
8696
|
-
}
|
|
9436
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
9437
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
8697
9438
|
const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
9439
|
+
const presetCommand = (presetId ? getCliTransportPreset(presetId).command.trim() : "") || AI_CONNECT_DEFAULT_CLI_COMMANDS[key] || void 0;
|
|
9440
|
+
try {
|
|
9441
|
+
return resolveManagedCommand(
|
|
9442
|
+
key,
|
|
9443
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
9444
|
+
route.transport.command,
|
|
9445
|
+
presetCommand
|
|
9446
|
+
);
|
|
9447
|
+
} catch {
|
|
9448
|
+
throw new AiConnectError(
|
|
9449
|
+
"validation_error",
|
|
9450
|
+
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
9451
|
+
);
|
|
8707
9452
|
}
|
|
8708
|
-
throw new AiConnectError(
|
|
8709
|
-
"validation_error",
|
|
8710
|
-
`No CLI command preset is registered for route "${route.id}". Provide transport.command or cli.commands.`
|
|
8711
|
-
);
|
|
8712
9453
|
}
|
|
8713
9454
|
function resolveCliExecutable(route, options) {
|
|
8714
9455
|
return splitCommandLine2(resolveCliCommand(route, options)).command;
|
|
8715
9456
|
}
|
|
8716
9457
|
function buildCliEnvironment(options) {
|
|
8717
|
-
return {
|
|
8718
|
-
...process.env,
|
|
8719
|
-
...options?.env ?? {}
|
|
8720
|
-
};
|
|
9458
|
+
return resolveChildEnv({ extra: options?.env ?? {} });
|
|
8721
9459
|
}
|
|
8722
9460
|
function buildCliCwd(context, options) {
|
|
8723
9461
|
return path2.resolve(
|
|
@@ -8818,9 +9556,13 @@ function resolveCliTransportOptions(route) {
|
|
|
8818
9556
|
} : void 0;
|
|
8819
9557
|
const merged = {
|
|
8820
9558
|
...preset?.options ?? {},
|
|
9559
|
+
// `...route.transport.cli` already spreads argsTemplate + parser (when set on
|
|
9560
|
+
// the route) over the preset defaults, so the previously-present conditional
|
|
9561
|
+
// re-spreads of those two fields were dead/redundant (they re-assigned the
|
|
9562
|
+
// identical value) and have been dropped. fileInput/preset below are NOT
|
|
9563
|
+
// redundant: fileInput is a per-field DEEP merge (mergedFileInput) and preset
|
|
9564
|
+
// is derived (route.transport.cli.preset ?? default), not a plain passthrough.
|
|
8821
9565
|
...route.transport.cli ?? {},
|
|
8822
|
-
...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
|
|
8823
|
-
...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
|
|
8824
9566
|
...mergedFileInput ? { fileInput: mergedFileInput } : {},
|
|
8825
9567
|
...presetId ? { preset: presetId } : {}
|
|
8826
9568
|
};
|
|
@@ -8917,7 +9659,7 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
|
|
|
8917
9659
|
}
|
|
8918
9660
|
return values.outputFile;
|
|
8919
9661
|
default:
|
|
8920
|
-
if (templateArg.startsWith("--")) {
|
|
9662
|
+
if (templateArg.startsWith("--") && templateArg.length > 2) {
|
|
8921
9663
|
parameterKeys.push(templateArg);
|
|
8922
9664
|
}
|
|
8923
9665
|
return templateArg;
|
|
@@ -9070,8 +9812,19 @@ function getValueByPath(value, dotPath) {
|
|
|
9070
9812
|
}
|
|
9071
9813
|
return current;
|
|
9072
9814
|
}
|
|
9073
|
-
function
|
|
9074
|
-
|
|
9815
|
+
function parseCliJson(raw, context) {
|
|
9816
|
+
try {
|
|
9817
|
+
return JSON.parse(raw);
|
|
9818
|
+
} catch (error) {
|
|
9819
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
9820
|
+
throw new AiConnectError(
|
|
9821
|
+
"temporary_unavailable",
|
|
9822
|
+
`${context} produced output that is not valid JSON (${detail}).`
|
|
9823
|
+
);
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
function splitJsonlLines(stdout, context = "CLI jsonl parser") {
|
|
9827
|
+
return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => parseCliJson(line, context));
|
|
9075
9828
|
}
|
|
9076
9829
|
function normalizeErrorMessage(value) {
|
|
9077
9830
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -9097,8 +9850,8 @@ function findJsonlSelection(entries, selector) {
|
|
|
9097
9850
|
}
|
|
9098
9851
|
return void 0;
|
|
9099
9852
|
}
|
|
9100
|
-
function parseGenericJsonCli(stdout, parser) {
|
|
9101
|
-
const payload =
|
|
9853
|
+
function parseGenericJsonCli(stdout, parser, context) {
|
|
9854
|
+
const payload = parseCliJson(stdout, context);
|
|
9102
9855
|
const errorValue = parser.errorPath ? getValueByPath(payload, parser.errorPath) : void 0;
|
|
9103
9856
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9104
9857
|
if (errorMessage) {
|
|
@@ -9119,8 +9872,8 @@ function parseGenericJsonCli(stdout, parser) {
|
|
|
9119
9872
|
data: payload
|
|
9120
9873
|
};
|
|
9121
9874
|
}
|
|
9122
|
-
function parseGenericJsonlCli(stdout, parser) {
|
|
9123
|
-
const entries = splitJsonlLines(stdout);
|
|
9875
|
+
function parseGenericJsonlCli(stdout, parser, context) {
|
|
9876
|
+
const entries = splitJsonlLines(stdout, context);
|
|
9124
9877
|
const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
|
|
9125
9878
|
const errorMessage = normalizeErrorMessage(errorValue);
|
|
9126
9879
|
if (errorMessage) {
|
|
@@ -9194,7 +9947,7 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9194
9947
|
);
|
|
9195
9948
|
}
|
|
9196
9949
|
const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
|
|
9197
|
-
const payload =
|
|
9950
|
+
const payload = parseCliJson(stdout, "CLI discovery json parser");
|
|
9198
9951
|
const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
|
|
9199
9952
|
return Array.isArray(arr) ? arr : [];
|
|
9200
9953
|
})();
|
|
@@ -9207,8 +9960,8 @@ function parseCliModelList(stdout, parser, selector) {
|
|
|
9207
9960
|
}
|
|
9208
9961
|
return models;
|
|
9209
9962
|
}
|
|
9210
|
-
function parseClaudeCli(stdout) {
|
|
9211
|
-
const payload =
|
|
9963
|
+
function parseClaudeCli(stdout, context) {
|
|
9964
|
+
const payload = parseCliJson(stdout, context);
|
|
9212
9965
|
const text = typeof payload.result === "string" ? payload.result : typeof payload.response === "string" ? payload.response : typeof payload.text === "string" ? payload.text : void 0;
|
|
9213
9966
|
if (!text) {
|
|
9214
9967
|
throw new AiConnectError(
|
|
@@ -9223,8 +9976,8 @@ function parseClaudeCli(stdout) {
|
|
|
9223
9976
|
data: payload
|
|
9224
9977
|
};
|
|
9225
9978
|
}
|
|
9226
|
-
function parseCodexCli(stdout, outputFileContent) {
|
|
9227
|
-
const events = splitJsonlLines(stdout);
|
|
9979
|
+
function parseCodexCli(stdout, outputFileContent, context) {
|
|
9980
|
+
const events = splitJsonlLines(stdout, context);
|
|
9228
9981
|
let text = outputFileContent?.trim() || void 0;
|
|
9229
9982
|
let usage;
|
|
9230
9983
|
for (const event of events) {
|
|
@@ -9274,6 +10027,7 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9274
10027
|
);
|
|
9275
10028
|
}
|
|
9276
10029
|
const cliOptions = resolveCliTransportOptions(route);
|
|
10030
|
+
const parseContext = `CLI transport "${route.transport.id}"${cliOptions.preset ? ` (preset ${cliOptions.preset})` : ""}`;
|
|
9277
10031
|
if (route.transport.cli?.parser) {
|
|
9278
10032
|
const parser = cliOptions.parser;
|
|
9279
10033
|
if (!parser) {
|
|
@@ -9286,9 +10040,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9286
10040
|
case "text":
|
|
9287
10041
|
return parseTextCli(result.stdout, parser);
|
|
9288
10042
|
case "json":
|
|
9289
|
-
return parseGenericJsonCli(stdout, parser);
|
|
10043
|
+
return parseGenericJsonCli(stdout, parser, parseContext);
|
|
9290
10044
|
default:
|
|
9291
|
-
return parseGenericJsonlCli(stdout, parser);
|
|
10045
|
+
return parseGenericJsonlCli(stdout, parser, parseContext);
|
|
9292
10046
|
}
|
|
9293
10047
|
}
|
|
9294
10048
|
switch (cliOptions.preset) {
|
|
@@ -9296,9 +10050,9 @@ function parseCliResult(route, result, outputFileContent) {
|
|
|
9296
10050
|
return parseTextCli(stdout, { kind: "text" });
|
|
9297
10051
|
case "claude":
|
|
9298
10052
|
case "openclaude":
|
|
9299
|
-
return parseClaudeCli(stdout);
|
|
10053
|
+
return parseClaudeCli(stdout, parseContext);
|
|
9300
10054
|
case "codex":
|
|
9301
|
-
return parseCodexCli(stdout, outputFileContent);
|
|
10055
|
+
return parseCodexCli(stdout, outputFileContent, parseContext);
|
|
9302
10056
|
default:
|
|
9303
10057
|
throw new AiConnectError(
|
|
9304
10058
|
"not_supported",
|
|
@@ -9312,29 +10066,27 @@ function capturePhase(phases, name, startedAt) {
|
|
|
9312
10066
|
durationMs: Date.now() - startedAt
|
|
9313
10067
|
});
|
|
9314
10068
|
}
|
|
10069
|
+
var MAX_CLI_OUTPUT_CHARS = 64 * 1024 * 1024;
|
|
9315
10070
|
async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
9316
10071
|
const spawnStartedAt = Date.now();
|
|
10072
|
+
const detached = process.platform !== "win32";
|
|
9317
10073
|
const child = spawn2(invocation.command, invocation.args, {
|
|
9318
10074
|
cwd: invocation.cwd,
|
|
9319
10075
|
env: invocation.env,
|
|
9320
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10076
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10077
|
+
detached
|
|
9321
10078
|
});
|
|
9322
10079
|
capturePhase(phases, "spawn", spawnStartedAt);
|
|
9323
10080
|
let stdout = "";
|
|
9324
10081
|
let stderr = "";
|
|
9325
10082
|
child.stdout.setEncoding("utf8");
|
|
9326
|
-
child.stdout.on("data", (chunk) => {
|
|
9327
|
-
stdout += chunk;
|
|
9328
|
-
});
|
|
9329
10083
|
child.stderr.setEncoding("utf8");
|
|
9330
|
-
child.stderr.on("data", (chunk) => {
|
|
9331
|
-
stderr += chunk;
|
|
9332
|
-
});
|
|
9333
10084
|
const executeStartedAt = Date.now();
|
|
9334
10085
|
return await new Promise((resolve, reject) => {
|
|
9335
10086
|
let abortListener;
|
|
10087
|
+
let overflowed = false;
|
|
9336
10088
|
const timer = setTimeout(() => {
|
|
9337
|
-
child
|
|
10089
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9338
10090
|
reject(
|
|
9339
10091
|
new AiConnectError(
|
|
9340
10092
|
"temporary_unavailable",
|
|
@@ -9349,15 +10101,43 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9349
10101
|
abortListener = void 0;
|
|
9350
10102
|
}
|
|
9351
10103
|
};
|
|
10104
|
+
const enforceBufferCap = () => {
|
|
10105
|
+
if (overflowed || stdout.length + stderr.length <= MAX_CLI_OUTPUT_CHARS) {
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
10108
|
+
overflowed = true;
|
|
10109
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
10110
|
+
cleanup();
|
|
10111
|
+
reject(
|
|
10112
|
+
new AiConnectError(
|
|
10113
|
+
"temporary_unavailable",
|
|
10114
|
+
`CLI transport "${quoteArg(invocation.command)}" exceeded the ${MAX_CLI_OUTPUT_CHARS}-character output cap.`
|
|
10115
|
+
)
|
|
10116
|
+
);
|
|
10117
|
+
};
|
|
10118
|
+
child.stdout.on("data", (chunk) => {
|
|
10119
|
+
if (overflowed) {
|
|
10120
|
+
return;
|
|
10121
|
+
}
|
|
10122
|
+
stdout += chunk;
|
|
10123
|
+
enforceBufferCap();
|
|
10124
|
+
});
|
|
10125
|
+
child.stderr.on("data", (chunk) => {
|
|
10126
|
+
if (overflowed) {
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
10129
|
+
stderr += chunk;
|
|
10130
|
+
enforceBufferCap();
|
|
10131
|
+
});
|
|
9352
10132
|
if (signal) {
|
|
9353
10133
|
if (signal.aborted) {
|
|
9354
|
-
child
|
|
10134
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9355
10135
|
cleanup();
|
|
9356
10136
|
reject(new AiConnectError("aborted", "CLI transport aborted before execution."));
|
|
9357
10137
|
return;
|
|
9358
10138
|
}
|
|
9359
10139
|
abortListener = () => {
|
|
9360
|
-
child
|
|
10140
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9361
10141
|
cleanup();
|
|
9362
10142
|
reject(new AiConnectError("aborted", "CLI transport aborted."));
|
|
9363
10143
|
};
|
|
@@ -9373,6 +10153,9 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
|
|
|
9373
10153
|
);
|
|
9374
10154
|
});
|
|
9375
10155
|
child.once("close", (exitCode) => {
|
|
10156
|
+
if (overflowed) {
|
|
10157
|
+
return;
|
|
10158
|
+
}
|
|
9376
10159
|
cleanup();
|
|
9377
10160
|
capturePhase(phases, "execute", executeStartedAt);
|
|
9378
10161
|
resolve({
|
|
@@ -9476,7 +10259,10 @@ function createCliTransportManager(options) {
|
|
|
9476
10259
|
)
|
|
9477
10260
|
);
|
|
9478
10261
|
} finally {
|
|
9479
|
-
|
|
10262
|
+
try {
|
|
10263
|
+
await cleanupCliInvocation(invocation);
|
|
10264
|
+
} catch {
|
|
10265
|
+
}
|
|
9480
10266
|
}
|
|
9481
10267
|
},
|
|
9482
10268
|
async runPrompt(context) {
|
|
@@ -9516,7 +10302,10 @@ function createCliTransportManager(options) {
|
|
|
9516
10302
|
}
|
|
9517
10303
|
return result;
|
|
9518
10304
|
} finally {
|
|
9519
|
-
|
|
10305
|
+
try {
|
|
10306
|
+
await cleanupCliInvocation(invocation);
|
|
10307
|
+
} catch {
|
|
10308
|
+
}
|
|
9520
10309
|
}
|
|
9521
10310
|
}
|
|
9522
10311
|
};
|
|
@@ -9543,28 +10332,21 @@ function splitCommandLine3(commandLine) {
|
|
|
9543
10332
|
};
|
|
9544
10333
|
}
|
|
9545
10334
|
function resolveServerCommand(route, options) {
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
const preset = AI_CONNECT_DEFAULT_SERVER_COMMANDS[`${route.provider}:${route.transport.id}`];
|
|
9561
|
-
if (preset) {
|
|
9562
|
-
return preset;
|
|
10335
|
+
const key = `${route.provider}:${route.transport.id}`;
|
|
10336
|
+
const optionsOverride = options?.commands?.[key]?.trim() || options?.commands?.[route.transport.id]?.trim() || options?.commands?.[route.provider]?.trim() || void 0;
|
|
10337
|
+
try {
|
|
10338
|
+
return resolveManagedCommand(
|
|
10339
|
+
key,
|
|
10340
|
+
optionsOverride ? { [key]: optionsOverride } : void 0,
|
|
10341
|
+
route.transport.command,
|
|
10342
|
+
AI_CONNECT_DEFAULT_SERVER_COMMANDS[key]
|
|
10343
|
+
);
|
|
10344
|
+
} catch {
|
|
10345
|
+
throw new AiConnectError(
|
|
10346
|
+
"validation_error",
|
|
10347
|
+
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
10348
|
+
);
|
|
9563
10349
|
}
|
|
9564
|
-
throw new AiConnectError(
|
|
9565
|
-
"validation_error",
|
|
9566
|
-
`No server command preset is registered for route "${route.id}". Provide transport.command or server.commands.`
|
|
9567
|
-
);
|
|
9568
10350
|
}
|
|
9569
10351
|
function resolveServerExecutable(route, options) {
|
|
9570
10352
|
return splitCommandLine3(resolveServerCommand(route, options)).command;
|
|
@@ -9636,9 +10418,12 @@ async function findAvailablePort(host = "127.0.0.1") {
|
|
|
9636
10418
|
async function waitForServerHealth(fetchLike, origin, timeoutMs) {
|
|
9637
10419
|
const deadline = Date.now() + timeoutMs;
|
|
9638
10420
|
let lastError;
|
|
10421
|
+
const perAttemptTimeoutMs = Math.min(2e3, Math.max(250, timeoutMs));
|
|
9639
10422
|
while (Date.now() < deadline) {
|
|
9640
10423
|
try {
|
|
9641
|
-
const response = await fetchLike(joinUrl(origin, "/global/health")
|
|
10424
|
+
const response = await fetchLike(joinUrl(origin, "/global/health"), {
|
|
10425
|
+
signal: AbortSignal.timeout(perAttemptTimeoutMs)
|
|
10426
|
+
});
|
|
9642
10427
|
if (response.ok) {
|
|
9643
10428
|
return;
|
|
9644
10429
|
}
|
|
@@ -9668,14 +10453,14 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9668
10453
|
}
|
|
9669
10454
|
args.push("--hostname", host, "--port", String(port));
|
|
9670
10455
|
const spawnStartedAt = Date.now();
|
|
10456
|
+
const detached = process.platform !== "win32";
|
|
9671
10457
|
const child = spawn3(resolved.command, args, {
|
|
9672
10458
|
cwd: cwdOverride ?? options?.cwd ?? process.cwd(),
|
|
9673
|
-
env: {
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
},
|
|
9677
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10459
|
+
env: resolveChildEnv({ extra: options?.env ?? {} }),
|
|
10460
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10461
|
+
detached
|
|
9678
10462
|
});
|
|
10463
|
+
const untrack = trackChild(child, detached);
|
|
9679
10464
|
let stderr = "";
|
|
9680
10465
|
child.stderr.setEncoding("utf8");
|
|
9681
10466
|
child.stderr.on("data", (chunk) => {
|
|
@@ -9691,7 +10476,8 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9691
10476
|
);
|
|
9692
10477
|
capturePhase2(phases, "health", healthStartedAt);
|
|
9693
10478
|
} catch (error) {
|
|
9694
|
-
|
|
10479
|
+
untrack();
|
|
10480
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9695
10481
|
throw new AiConnectError(
|
|
9696
10482
|
"local_harness_unavailable",
|
|
9697
10483
|
stderr.trim() || (error instanceof Error ? error.message : "Failed to start local server transport.")
|
|
@@ -9699,15 +10485,17 @@ async function spawnServerInstance(route, options, phases, cwdOverride) {
|
|
|
9699
10485
|
}
|
|
9700
10486
|
return {
|
|
9701
10487
|
origin,
|
|
10488
|
+
child,
|
|
9702
10489
|
dispose: async () => {
|
|
10490
|
+
untrack();
|
|
9703
10491
|
if (child.killed) {
|
|
9704
10492
|
return;
|
|
9705
10493
|
}
|
|
9706
|
-
child
|
|
10494
|
+
killProcessTree(child, "SIGTERM", detached);
|
|
9707
10495
|
await new Promise((resolve) => {
|
|
9708
10496
|
child.once("close", () => resolve());
|
|
9709
10497
|
setTimeout(() => {
|
|
9710
|
-
child
|
|
10498
|
+
killProcessTree(child, "SIGKILL", detached);
|
|
9711
10499
|
resolve();
|
|
9712
10500
|
}, 1e3).unref?.();
|
|
9713
10501
|
});
|
|
@@ -9807,7 +10595,17 @@ function createServerTransportManager(options) {
|
|
|
9807
10595
|
const pending = spawnServerInstance(route, options, phases, cwdOverride);
|
|
9808
10596
|
instances.set(key, pending);
|
|
9809
10597
|
try {
|
|
9810
|
-
|
|
10598
|
+
const instance = await pending;
|
|
10599
|
+
if (instance.child) {
|
|
10600
|
+
const evict = () => {
|
|
10601
|
+
if (instances.get(key) === pending) {
|
|
10602
|
+
instances.delete(key);
|
|
10603
|
+
}
|
|
10604
|
+
};
|
|
10605
|
+
instance.child.once("exit", evict);
|
|
10606
|
+
instance.child.once("error", evict);
|
|
10607
|
+
}
|
|
10608
|
+
return instance;
|
|
9811
10609
|
} catch (error) {
|
|
9812
10610
|
instances.delete(key);
|
|
9813
10611
|
throw error;
|
|
@@ -9837,7 +10635,10 @@ function createServerTransportManager(options) {
|
|
|
9837
10635
|
headers: {
|
|
9838
10636
|
"content-type": "application/json"
|
|
9839
10637
|
},
|
|
9840
|
-
body: JSON.stringify({})
|
|
10638
|
+
body: JSON.stringify({}),
|
|
10639
|
+
// H6: honor caller cancel / operation timeout on session creation too
|
|
10640
|
+
// (was only wired on the message fetch).
|
|
10641
|
+
signal: context.abort.signal
|
|
9841
10642
|
});
|
|
9842
10643
|
capturePhase2(phases, "session/create", sessionStartedAt);
|
|
9843
10644
|
if (!sessionResponse.ok) {
|
|
@@ -9906,7 +10707,10 @@ function createServerTransportManager(options) {
|
|
|
9906
10707
|
};
|
|
9907
10708
|
context.telemetry?.captureTransport(transport);
|
|
9908
10709
|
const startedAt = Date.now();
|
|
9909
|
-
const response = await fetchLike(
|
|
10710
|
+
const response = await fetchLike(
|
|
10711
|
+
joinUrl(instance.origin, "/config/providers"),
|
|
10712
|
+
{ signal: context.abort.signal }
|
|
10713
|
+
);
|
|
9910
10714
|
capturePhase2(phases, "config/providers", startedAt);
|
|
9911
10715
|
if (!response.ok) {
|
|
9912
10716
|
throw new AiConnectError(
|
|
@@ -9950,6 +10754,15 @@ function createServerTransportManager(options) {
|
|
|
9950
10754
|
}
|
|
9951
10755
|
|
|
9952
10756
|
// src/local-handlers.ts
|
|
10757
|
+
function normalizeVerifyIssue(error, fallbackCode, fallbackMessage) {
|
|
10758
|
+
const normalized = error instanceof AiConnectError ? error : new AiConnectError(fallbackCode, fallbackMessage);
|
|
10759
|
+
return [
|
|
10760
|
+
{
|
|
10761
|
+
code: "handler_issue",
|
|
10762
|
+
message: normalized.message
|
|
10763
|
+
}
|
|
10764
|
+
];
|
|
10765
|
+
}
|
|
9953
10766
|
function createLocalRouteHandlers(options = {}) {
|
|
9954
10767
|
const acpTransport = createAcpTransportManager(options.acp);
|
|
9955
10768
|
const cliTransport = createCliTransportManager(options.cli);
|
|
@@ -9980,16 +10793,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
9980
10793
|
}
|
|
9981
10794
|
];
|
|
9982
10795
|
} catch (error) {
|
|
9983
|
-
|
|
10796
|
+
return normalizeVerifyIssue(
|
|
10797
|
+
error,
|
|
9984
10798
|
"validation_error",
|
|
9985
10799
|
"ACP route verification failed."
|
|
9986
10800
|
);
|
|
9987
|
-
return [
|
|
9988
|
-
{
|
|
9989
|
-
code: "handler_issue",
|
|
9990
|
-
message: normalized.message
|
|
9991
|
-
}
|
|
9992
|
-
];
|
|
9993
10801
|
}
|
|
9994
10802
|
}
|
|
9995
10803
|
},
|
|
@@ -10028,16 +10836,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
10028
10836
|
}
|
|
10029
10837
|
];
|
|
10030
10838
|
} catch (error) {
|
|
10031
|
-
|
|
10839
|
+
return normalizeVerifyIssue(
|
|
10840
|
+
error,
|
|
10032
10841
|
"validation_error",
|
|
10033
10842
|
"CLI route verification failed."
|
|
10034
10843
|
);
|
|
10035
|
-
return [
|
|
10036
|
-
{
|
|
10037
|
-
code: "handler_issue",
|
|
10038
|
-
message: normalized.message
|
|
10039
|
-
}
|
|
10040
|
-
];
|
|
10041
10844
|
}
|
|
10042
10845
|
}
|
|
10043
10846
|
},
|
|
@@ -10073,16 +10876,11 @@ function createLocalRouteHandlers(options = {}) {
|
|
|
10073
10876
|
}
|
|
10074
10877
|
];
|
|
10075
10878
|
} catch (error) {
|
|
10076
|
-
|
|
10879
|
+
return normalizeVerifyIssue(
|
|
10880
|
+
error,
|
|
10077
10881
|
"validation_error",
|
|
10078
10882
|
"Server route verification failed."
|
|
10079
10883
|
);
|
|
10080
|
-
return [
|
|
10081
|
-
{
|
|
10082
|
-
code: "handler_issue",
|
|
10083
|
-
message: normalized.message
|
|
10084
|
-
}
|
|
10085
|
-
];
|
|
10086
10884
|
}
|
|
10087
10885
|
}
|
|
10088
10886
|
}
|