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