@chatcode/cco-llm-chatcode-config 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/README.zh.md +133 -0
  4. package/cordis.patch.yml +4 -0
  5. package/cordis.web.patch.yml +12 -0
  6. package/docs/chatcode-login.md +88 -0
  7. package/docs/chatcode-login.zh.md +179 -0
  8. package/docs/chatcode-models.md +29 -0
  9. package/docs/chatcode-models.zh.md +29 -0
  10. package/docs/chatcode-reporting.md +96 -0
  11. package/docs/chatcode-reporting.zh.md +96 -0
  12. package/docs/decisions/2026-08-31-chatcode-model-source.md +39 -0
  13. package/docs/decisions/2026-08-31-chatcode-model-source.zh.md +39 -0
  14. package/docs/decisions/2026-09-16-actual-model-adapter-routing.md +31 -0
  15. package/docs/decisions/2026-09-16-actual-model-adapter-routing.zh.md +31 -0
  16. package/lib/client.js +469 -0
  17. package/lib/index.d.ts +263 -0
  18. package/lib/index.d.ts.map +1 -0
  19. package/lib/index.js +4873 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/startup-gate-BaCbWaKH.js +164 -0
  22. package/lib/startup-gate-BaCbWaKH.js.map +1 -0
  23. package/lib/web-startup.d.ts +9 -0
  24. package/lib/web-startup.d.ts.map +1 -0
  25. package/lib/web-startup.js +20 -0
  26. package/lib/web-startup.js.map +1 -0
  27. package/package.json +121 -0
  28. package/vendor/README.md +7 -0
  29. package/vendor/dsh-llm-pi-ai/LICENSE +21 -0
  30. package/vendor/dsh-llm-pi-ai/README.i18n.yaml +6 -0
  31. package/vendor/dsh-llm-pi-ai/README.md +238 -0
  32. package/vendor/dsh-llm-pi-ai/README.zh.md +238 -0
  33. package/vendor/dsh-llm-pi-ai/package.json +65 -0
  34. package/vendor/dsh-llm-pi-ai/src/adapter.ts +434 -0
  35. package/vendor/dsh-llm-pi-ai/src/auth.ts +241 -0
  36. package/vendor/dsh-llm-pi-ai/src/catalog.ts +908 -0
  37. package/vendor/dsh-llm-pi-ai/src/config.ts +478 -0
  38. package/vendor/dsh-llm-pi-ai/src/context.ts +349 -0
  39. package/vendor/dsh-llm-pi-ai/src/discovery.ts +284 -0
  40. package/vendor/dsh-llm-pi-ai/src/index.ts +336 -0
  41. package/vendor/dsh-llm-pi-ai/src/invariant.ts +30 -0
  42. package/vendor/dsh-llm-pi-ai/src/login.ts +161 -0
  43. package/vendor/dsh-llm-pi-ai/src/provider.ts +192 -0
  44. package/vendor/dsh-llm-pi-ai/src/replay.ts +249 -0
  45. package/vendor/dsh-llm-pi-ai/src/stream.ts +232 -0
  46. package/vendor/dsh-llm-pi-ai/tests/adapter.e2e.ts +168 -0
  47. package/vendor/dsh-llm-pi-ai/tests/adapter.spec.ts +1034 -0
  48. package/vendor/dsh-llm-pi-ai/tests/assemble.ts +32 -0
  49. package/vendor/dsh-llm-pi-ai/tests/auth-double.ts +39 -0
  50. package/vendor/dsh-llm-pi-ai/tests/auth.spec.ts +221 -0
  51. package/vendor/dsh-llm-pi-ai/tests/catalog.spec.ts +1220 -0
  52. package/vendor/dsh-llm-pi-ai/tests/config.spec.ts +111 -0
  53. package/vendor/dsh-llm-pi-ai/tests/context.spec.ts +474 -0
  54. package/vendor/dsh-llm-pi-ai/tests/convert.spec.ts +922 -0
  55. package/vendor/dsh-llm-pi-ai/tests/discovery.spec.ts +374 -0
  56. package/vendor/dsh-llm-pi-ai/tests/dynamic-config.spec.ts +241 -0
  57. package/vendor/dsh-llm-pi-ai/tests/fixtures/qr-code.png +0 -0
  58. package/vendor/dsh-llm-pi-ai/tests/loader-composition.spec.ts +244 -0
  59. package/vendor/dsh-llm-pi-ai/tests/login.spec.ts +198 -0
  60. package/vendor/dsh-llm-pi-ai/tests/mock-server.ts +82 -0
  61. package/vendor/dsh-llm-pi-ai/tests/provider-apis.e2e.ts +266 -0
  62. package/vendor/dsh-llm-pi-ai/tests/sdk-options.spec.ts +106 -0
  63. package/vendor/dsh-llm-pi-ai/tsconfig.json +4 -0
  64. package/vendor/dsh-llm-pi-ai/tsconfig.upstream.json +51 -0
package/lib/index.js ADDED
@@ -0,0 +1,4873 @@
1
+ import { n as checkChatCodeStartupGate, r as startupGateFailureMessage, t as ChatCodeStartupGateService } from "./startup-gate-BaCbWaKH.js";
2
+ import { createRequire } from "node:module";
3
+ import { Service } from "@deepseek-ai/cordis";
4
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { emitKeypressEvents } from "node:readline";
7
+ import { stdin, stdout } from "node:process";
8
+ import { homedir, userInfo } from "node:os";
9
+ import { mkdir, open, readFile, readdir, rename, unlink } from "node:fs/promises";
10
+ import { basename, extname, isAbsolute, join, resolve } from "node:path";
11
+ import { InMemoryCredentialStore, createModels, createProvider, defaultProviderAuthContext, getSupportedThinkingLevels, isContextOverflow } from "@earendil-works/pi-ai";
12
+ import { credentialKey, credentialRef } from "@deepseek-ai/dsh-credentials";
13
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
14
+ import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
15
+ import { DeepSeekAdapter, resolveAdapterOptions } from "@deepseek-ai/dsh-llm-deepseek";
16
+ import { MAX_TIMER_DELAY_MS, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
17
+ import { brandString } from "@deepseek-ai/dsh-brand";
18
+ import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
19
+ import z from "@deepseek-ai/schemastery";
20
+ import { builtinProviders, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
21
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
22
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
23
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
24
+ import { createHash, randomUUID } from "node:crypto";
25
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
26
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
27
+ //#region src/update.ts
28
+ /** Self-update for the global ChatCode CLI installation. @module dsh-llm-chatcode-config/update */
29
+ /**
30
+ * The public ChatCode CLI package owns both browser and terminal surfaces.
31
+ */
32
+ const CHATCODE_CLI_PACKAGES = ["@chatcode/chatcode-cli"];
33
+ /** Bound one npm call so a stalled registry cannot hang the command forever. */
34
+ const VIEW_TIMEOUT_MS = 2e4;
35
+ const LIST_TIMEOUT_MS = 15e3;
36
+ const INSTALL_TIMEOUT_MS = 12e4;
37
+ /** Print one progress line to the terminal while an update runs. */
38
+ function log(message) {
39
+ process.stdout.write(`[update] ${message}\n`);
40
+ }
41
+ /**
42
+ * Run one npm invocation and capture its exit code and combined output.
43
+ * Registry, auth, and proxy resolution are npm's own business: no registry is
44
+ * forced, so a project's `.npmrc` (mirror, scoped registry, token) applies.
45
+ * @param args - npm arguments after the `npm` command word.
46
+ * @param timeoutMs - bound on the whole invocation.
47
+ */
48
+ function runNpm(args, timeoutMs, signal) {
49
+ return new Promise((resolve) => {
50
+ const win = process.platform === "win32";
51
+ const stdio = [
52
+ "ignore",
53
+ "pipe",
54
+ "pipe"
55
+ ];
56
+ let child;
57
+ if (win) {
58
+ const spec = args.map((argument) => /\s/.test(argument) ? JSON.stringify(argument) : argument).join(" ");
59
+ child = spawn(`npm ${spec}`, {
60
+ shell: true,
61
+ stdio
62
+ });
63
+ } else child = spawn("npm", args, { stdio });
64
+ let output = "";
65
+ let done = false;
66
+ const settle = (code) => {
67
+ if (done) return;
68
+ done = true;
69
+ clearTimeout(timer);
70
+ if (signal !== void 0) signal.removeEventListener("abort", onAbort);
71
+ resolve({
72
+ code,
73
+ output
74
+ });
75
+ };
76
+ const timer = setTimeout(() => {
77
+ child.kill();
78
+ }, timeoutMs);
79
+ const onAbort = () => {
80
+ clearTimeout(timer);
81
+ child.kill();
82
+ };
83
+ if (signal !== void 0) {
84
+ if (signal.aborted) onAbort();
85
+ else signal.addEventListener("abort", onAbort, { once: true });
86
+ }
87
+ const collect = (chunk) => {
88
+ output += chunk.toString();
89
+ };
90
+ child.stdout?.on("data", collect);
91
+ child.stderr?.on("data", collect);
92
+ child.on("error", (error) => {
93
+ output += String(error);
94
+ settle(-1);
95
+ });
96
+ child.on("close", (code) => {
97
+ settle(code ?? -1);
98
+ });
99
+ });
100
+ }
101
+ /** Strip npm's advisory `npm warn` lines from captured combined output. */
102
+ function cleanNpmOutput(output) {
103
+ return output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "" && !/^npm warn\b/i.test(line)).join("\n");
104
+ }
105
+ /**
106
+ * Turn npm's noisy combined output into one readable failure line. `--json`
107
+ * errors print `{ "error": { "code", "summary", "detail" } }` on stderr,
108
+ * possibly surrounded by `npm error` prose; lift the first brace block and read
109
+ * the summary from it, else fall back to the cleaned raw text.
110
+ */
111
+ function describeNpmFailure(output, packageName) {
112
+ const start = output.indexOf("{");
113
+ const end = output.lastIndexOf("}");
114
+ if (start !== -1 && end > start) try {
115
+ const parsed = JSON.parse(output.slice(start, end + 1));
116
+ const code = typeof parsed.error?.code === "string" ? parsed.error.code : void 0;
117
+ const summary = typeof parsed.error?.summary === "string" ? parsed.error.summary : void 0;
118
+ if (code !== void 0 || summary !== void 0) return `${summary ?? code}(${packageName})`;
119
+ } catch {}
120
+ return `${cleanNpmOutput(output) || "npm view 失败"}(${packageName})`;
121
+ }
122
+ /**
123
+ * Fetch the latest published version of one package through `npm view`, so the
124
+ * same registry, scoped-registry override, and auth token the installer uses
125
+ * also decide what "latest" means.
126
+ */
127
+ async function fetchLatestVersion(packageName, signal) {
128
+ const result = await runNpm([
129
+ "view",
130
+ packageName,
131
+ "version",
132
+ "--json"
133
+ ], VIEW_TIMEOUT_MS, signal);
134
+ if (result.code !== 0 || result.output.trim() === "") throw new Error(describeNpmFailure(result.output, packageName));
135
+ const stdout = cleanNpmOutput(result.output);
136
+ try {
137
+ const parsed = JSON.parse(stdout);
138
+ if (typeof parsed === "string" && parsed !== "") return parsed;
139
+ } catch {}
140
+ throw new Error(`无法从 registry 解析最新版本(${packageName})`);
141
+ }
142
+ /** Best-effort read of one installed package version from a co-located install. */
143
+ function resolveCoLocatedVersion(packageName) {
144
+ try {
145
+ const manifest = createRequire(import.meta.url)(`${packageName}/package.json`);
146
+ return typeof manifest.version === "string" ? manifest.version : void 0;
147
+ } catch {
148
+ return;
149
+ }
150
+ }
151
+ /**
152
+ * Extract the `--json` result from npm's combined output. npm can prefix
153
+ * advisory `npm warn` lines on stderr (e.g. from a pnpm-injected env config),
154
+ * so lift the first brace block and parse just that, not the whole stream.
155
+ */
156
+ function parseNpmJson(output) {
157
+ const start = output.indexOf("{");
158
+ const end = output.lastIndexOf("}");
159
+ if (start === -1 || end <= start) return void 0;
160
+ try {
161
+ return JSON.parse(output.slice(start, end + 1));
162
+ } catch {
163
+ return;
164
+ }
165
+ }
166
+ /** Resolve one installed global package version through npm's own store. */
167
+ async function resolveGlobalVersion(packageName, signal) {
168
+ const result = await runNpm([
169
+ "list",
170
+ "-g",
171
+ packageName,
172
+ "--json",
173
+ "--depth=0"
174
+ ], LIST_TIMEOUT_MS, signal);
175
+ if (result.output.trim() === "") return void 0;
176
+ const version = parseNpmJson(result.output)?.dependencies?.[packageName]?.version;
177
+ return typeof version === "string" ? version : void 0;
178
+ }
179
+ /**
180
+ * Best-effort read of one installed package version; `undefined` when it cannot
181
+ * be resolved. The co-located probe first targets a dev tree, then falls back
182
+ * to npm's view of the global install.
183
+ */
184
+ async function resolveCurrentVersion(packageName, signal) {
185
+ return resolveCoLocatedVersion(packageName) ?? await resolveGlobalVersion(packageName, signal);
186
+ }
187
+ /** Run `npm install -g <specs...>`, letting npm pick the registry it would install from. */
188
+ function runNpmInstall(packageSpecs, signal) {
189
+ return runNpm([
190
+ "install",
191
+ "-g",
192
+ ...packageSpecs
193
+ ], INSTALL_TIMEOUT_MS, signal);
194
+ }
195
+ /**
196
+ * Check the latest version and, when newer than the running one, install the
197
+ * public ChatCode CLI package. Progress lines go to `options.onProgress` when given (the
198
+ * interactive surface renders them in a panel), else to stdout — so the
199
+ * `chatcode-cli --update` sync entry point keeps printing as before. Every awaited step
200
+ * honors `options.signal`: an abort kills the in-flight npm child and settles
201
+ * with the `aborted` status.
202
+ */
203
+ async function runUpdate(options = {}) {
204
+ const { signal, onProgress } = options;
205
+ const progress = (line) => {
206
+ if (onProgress !== void 0) onProgress(line);
207
+ else log(line);
208
+ };
209
+ const aborted = () => signal?.aborted === true ? {
210
+ status: "aborted",
211
+ message: "更新已中止"
212
+ } : void 0;
213
+ progress(`开始检查更新:${CHATCODE_CLI_PACKAGES.join("、")}`);
214
+ const updates = [];
215
+ for (const name of CHATCODE_CLI_PACKAGES) {
216
+ if (aborted() !== void 0) return aborted();
217
+ const current = await resolveCurrentVersion(name, signal);
218
+ if (aborted() !== void 0) return aborted();
219
+ progress(`当前版本 ${name}:${current ?? "(未安装或未解析)"}`);
220
+ let latest;
221
+ try {
222
+ latest = await fetchLatestVersion(name, signal);
223
+ } catch (error) {
224
+ const detail = error instanceof Error ? error.message : String(error);
225
+ progress(`检查 ${name} 最新版本失败:${detail}`);
226
+ return {
227
+ status: "error",
228
+ message: `检查更新失败:${detail}`
229
+ };
230
+ }
231
+ if (aborted() !== void 0) return aborted();
232
+ progress(`最新版本 ${name}:${latest}`);
233
+ updates.push({
234
+ name,
235
+ current,
236
+ latest
237
+ });
238
+ }
239
+ if (!updates.some((update) => update.current !== update.latest)) {
240
+ const summary = updates.map((update) => `${update.name} ${update.latest}`).join(",");
241
+ progress("ChatCode CLI 已是最新版本,无需更新");
242
+ return {
243
+ status: "up-to-date",
244
+ message: `ChatCode CLI 已是最新版本(${summary})`
245
+ };
246
+ }
247
+ const packageSpecs = updates.map((update) => `${update.name}@${update.latest}`);
248
+ progress(`检测到新版本,开始安装:${packageSpecs.join(" ")}`);
249
+ const result = await runNpmInstall(packageSpecs, signal);
250
+ if (aborted() !== void 0) return aborted();
251
+ if (result.code !== 0) {
252
+ const detail = cleanNpmOutput(result.output) || "未知错误";
253
+ progress(`安装失败(exit ${result.code})`);
254
+ return {
255
+ status: "error",
256
+ message: `更新失败:${detail}\n请手动执行:npm install -g ${packageSpecs.join(" ")}`
257
+ };
258
+ }
259
+ progress("安装完成");
260
+ return {
261
+ status: "updated",
262
+ message: `已更新:${updates.map((update) => `${update.name} ${update.current ?? "(未知)"} → ${update.latest}`).join(",")},请重启 ChatCode CLI 以生效`
263
+ };
264
+ }
265
+ /** Run one npm invocation synchronously and capture its exit code and output. */
266
+ function syncRunNpm(args, timeoutMs) {
267
+ const win = process.platform === "win32";
268
+ const spec = args.map((argument) => /\s/.test(argument) ? JSON.stringify(argument) : argument).join(" ");
269
+ const result = win ? spawnSync(`npm ${spec}`, {
270
+ shell: true,
271
+ encoding: "utf8",
272
+ timeout: timeoutMs,
273
+ stdio: [
274
+ "ignore",
275
+ "pipe",
276
+ "pipe"
277
+ ]
278
+ }) : spawnSync("npm", args, {
279
+ encoding: "utf8",
280
+ timeout: timeoutMs,
281
+ stdio: [
282
+ "ignore",
283
+ "pipe",
284
+ "pipe"
285
+ ]
286
+ });
287
+ if (result.error !== void 0) return {
288
+ code: -1,
289
+ output: String(result.error)
290
+ };
291
+ return {
292
+ code: result.status ?? -1,
293
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`
294
+ };
295
+ }
296
+ /**
297
+ * Print one progress line without yielding the event loop. On a Windows
298
+ * console `process.stdout.write` still issues the console write immediately
299
+ * (libuv calls WriteConsoleW synchronously), so the bytes are on screen before
300
+ * the caller's hard exit — and because we never yield, the concurrently booting
301
+ * profile stays frozen and cannot print its own logs.
302
+ */
303
+ function syncLog(message) {
304
+ process.stdout.write(`[update] ${message}\n`);
305
+ }
306
+ /** Resolve one installed global package version synchronously through npm's store. */
307
+ function resolveGlobalVersionSync(packageName) {
308
+ const result = syncRunNpm([
309
+ "list",
310
+ "-g",
311
+ packageName,
312
+ "--json",
313
+ "--depth=0"
314
+ ], LIST_TIMEOUT_MS);
315
+ if (result.output.trim() === "") return void 0;
316
+ const version = parseNpmJson(result.output)?.dependencies?.[packageName]?.version;
317
+ return typeof version === "string" ? version : void 0;
318
+ }
319
+ /**
320
+ * Synchronous twin of {@link runUpdate} for `chatcode-cli --update`. Blocking the event
321
+ * loop on purpose: the profile's other plugins boot concurrently with this one,
322
+ * and `chatcode-cli --update` must finish — and exit — before any of them can print.
323
+ * The final message is returned, not logged, so the caller writes it once and
324
+ * picks the exit code.
325
+ */
326
+ function runUpdateSync() {
327
+ syncLog(`开始检查更新:${CHATCODE_CLI_PACKAGES.join("、")}`);
328
+ const updates = [];
329
+ for (const name of CHATCODE_CLI_PACKAGES) {
330
+ const current = resolveCoLocatedVersion(name) ?? resolveGlobalVersionSync(name);
331
+ syncLog(`当前版本 ${name}:${current ?? "(未安装或未解析)"}`);
332
+ const view = syncRunNpm([
333
+ "view",
334
+ name,
335
+ "version",
336
+ "--json"
337
+ ], VIEW_TIMEOUT_MS);
338
+ let latest;
339
+ if (view.code !== 0 || view.output.trim() === "") {
340
+ const detail = describeNpmFailure(view.output, name);
341
+ syncLog(`检查 ${name} 最新版本失败:${detail}`);
342
+ return {
343
+ status: "error",
344
+ message: `检查更新失败:${detail}`
345
+ };
346
+ }
347
+ try {
348
+ const parsed = JSON.parse(cleanNpmOutput(view.output));
349
+ if (typeof parsed !== "string" || parsed === "") throw new Error("invalid version payload");
350
+ latest = parsed;
351
+ } catch {
352
+ const detail = `无法从 registry 解析最新版本(${name})`;
353
+ syncLog(`检查 ${name} 最新版本失败:${detail}`);
354
+ return {
355
+ status: "error",
356
+ message: `检查更新失败:${detail}`
357
+ };
358
+ }
359
+ syncLog(`最新版本 ${name}:${latest}`);
360
+ updates.push({
361
+ name,
362
+ current,
363
+ latest
364
+ });
365
+ }
366
+ if (!updates.some((update) => update.current !== update.latest)) {
367
+ const summary = updates.map((update) => `${update.name} ${update.latest}`).join(",");
368
+ syncLog("ChatCode CLI 已是最新版本,无需更新");
369
+ return {
370
+ status: "up-to-date",
371
+ message: `ChatCode CLI 已是最新版本(${summary})`
372
+ };
373
+ }
374
+ const packageSpecs = updates.map((update) => `${update.name}@${update.latest}`);
375
+ syncLog(`检测到新版本,开始安装:${packageSpecs.join(" ")}`);
376
+ const install = syncRunNpm([
377
+ "install",
378
+ "-g",
379
+ ...packageSpecs
380
+ ], INSTALL_TIMEOUT_MS);
381
+ if (install.code !== 0) {
382
+ const detail = cleanNpmOutput(install.output) || "未知错误";
383
+ syncLog(`安装失败(exit ${install.code})`);
384
+ return {
385
+ status: "error",
386
+ message: `更新失败:${detail}\n请手动执行:npm install -g ${packageSpecs.join(" ")}`
387
+ };
388
+ }
389
+ syncLog("安装完成");
390
+ return {
391
+ status: "updated",
392
+ message: `已更新:${updates.map((update) => `${update.name} ${update.current ?? "(未知)"} → ${update.latest}`).join(",")},请重启 ChatCode CLI 以生效`
393
+ };
394
+ }
395
+ //#endregion
396
+ //#region src/version-check.ts
397
+ /**
398
+ * Startup version admission against the CVP version-validate endpoint.
399
+ *
400
+ * Mirrors the `yuanjing-wanma-cli` startup flow: after launch, ask CVP whether
401
+ * the installed ChatCode CLI package is still usable. When the server reports a newer
402
+ * enabled version (status 1) or a disabled current version with a rollback
403
+ * (status -1), present a keyboard dialog and either install the server-selected
404
+ * version or leave the program before the interactive surface mounts.
405
+ * @module dsh-llm-chatcode-config/version-check
406
+ */
407
+ const ERROR_CODE = 500;
408
+ function validateUrl(cvpChatCodeApiUrl) {
409
+ const base = cvpChatCodeApiUrl.replace(/\/+$/u, "");
410
+ return new URL(`${base}/chatcode/api/v1/cli/version/validate`);
411
+ }
412
+ /**
413
+ * Interpret one 200 JSON body. Returns nothing when the server said "valid",
414
+ * "unknown package/version", or errored — every such case leaves startup alone,
415
+ * matching the reference's fail-open behavior.
416
+ */
417
+ function decide(body) {
418
+ if (body.code === ERROR_CODE) return void 0;
419
+ const data = body.data;
420
+ const payload = data !== null && typeof data === "object" ? data : body;
421
+ if (typeof payload.version === "string" && payload.version !== "") {
422
+ if (payload.status === 1) return {
423
+ action: "upgrade",
424
+ version: payload.version
425
+ };
426
+ if (payload.status === -1) return {
427
+ action: "rollback",
428
+ version: payload.version
429
+ };
430
+ }
431
+ }
432
+ async function validatePackage(config, packageName, options) {
433
+ const resolveVersion = options.resolveVersion ?? resolveCurrentVersion;
434
+ let versionNum;
435
+ try {
436
+ versionNum = await resolveVersion(packageName);
437
+ } catch {
438
+ return;
439
+ }
440
+ if (versionNum === void 0 || versionNum === "") return void 0;
441
+ const url = validateUrl(config.cvpChatCodeApiUrl);
442
+ url.searchParams.set("packageName", packageName);
443
+ url.searchParams.set("versionNum", versionNum);
444
+ console.log(`[version-check] 开始校验 ${packageName}@${versionNum}`);
445
+ const request = options.request ?? fetch;
446
+ let response;
447
+ try {
448
+ response = await request(url, { signal: AbortSignal.timeout(options.timeoutMs ?? 1e4) });
449
+ } catch (err) {
450
+ console.log(`[version-check] 请求失败 (${packageName}): ${err?.message ?? err}`);
451
+ return;
452
+ }
453
+ if (!response.ok) return void 0;
454
+ const body = await response.json().catch(() => void 0);
455
+ if (body === null || typeof body !== "object") return void 0;
456
+ const wrapped = decide(body);
457
+ return wrapped === void 0 ? void 0 : {
458
+ ...wrapped,
459
+ currentVersion: versionNum
460
+ };
461
+ }
462
+ /**
463
+ * Validate the ChatCode CLI package and collapse results into one decision.
464
+ * A disabled version wins over an available upgrade: an unusable install must
465
+ * be replaced before any forward update matters.
466
+ */
467
+ async function checkVersions(config, options = {}) {
468
+ const outcomes = (await Promise.all(CHATCODE_CLI_PACKAGES.map(async (name) => {
469
+ const outcome = await validatePackage(config, name, options);
470
+ return outcome === void 0 ? void 0 : {
471
+ name,
472
+ action: outcome.action,
473
+ currentVersion: outcome.currentVersion,
474
+ targetVersion: outcome.version
475
+ };
476
+ }))).filter((entry) => entry !== void 0);
477
+ const rollbacks = outcomes.filter((entry) => entry.action === "rollback");
478
+ if (rollbacks.length > 0) return {
479
+ action: "rollback",
480
+ packages: rollbacks.map(({ name, currentVersion, targetVersion }) => ({
481
+ name,
482
+ currentVersion,
483
+ targetVersion
484
+ }))
485
+ };
486
+ const upgrades = outcomes.filter((entry) => entry.action === "upgrade");
487
+ if (upgrades.length > 0) return {
488
+ action: "upgrade",
489
+ packages: upgrades.map(({ name, currentVersion, targetVersion }) => ({
490
+ name,
491
+ currentVersion,
492
+ targetVersion
493
+ }))
494
+ };
495
+ }
496
+ /** Install the server-selected versions for one decision. */
497
+ async function runDecisionInstall(decision) {
498
+ const specs = decision.packages.map((pkg) => `${pkg.name}@${pkg.targetVersion}`);
499
+ const result = await runNpmInstall(specs);
500
+ if (result.code !== 0) return {
501
+ ok: false,
502
+ message: `安装失败:${result.output.trim() || "未知错误"}\n请手动执行:npm install -g ${specs.join(" ")}`
503
+ };
504
+ const summary = decision.packages.map((pkg) => `${pkg.name} ${pkg.targetVersion}`).join("、");
505
+ return {
506
+ ok: true,
507
+ message: `已${decision.action === "upgrade" ? "升级" : "更换"}到 ${summary},请重启 ChatCode CLI 以生效`
508
+ };
509
+ }
510
+ /** Localize one decision into the terminal dialog copy. */
511
+ function decisionPrompt(decision) {
512
+ const targets = decision.packages.map((pkg) => `${pkg.name} ${pkg.targetVersion}`).join("、");
513
+ if (decision.action === "upgrade") return {
514
+ title: "版本升级提醒",
515
+ message: `新版本已发布(${targets}),请升级后再使用。`,
516
+ performLabel: "升级",
517
+ cancelLabel: "不升级(退出程序)"
518
+ };
519
+ return {
520
+ title: "版本禁用提醒",
521
+ message: `当前版本已禁用(${decision.packages.map((pkg) => `${pkg.name} ${pkg.currentVersion}`).join("、")}),请更换到(${targets})后再使用。`,
522
+ performLabel: "更换",
523
+ cancelLabel: "不更换(退出程序)"
524
+ };
525
+ }
526
+ /**
527
+ * Show a two-option keyboard dialog: up/down arrows move the cursor, Enter
528
+ * chooses. Selecting the first option returns `perform`; the second option or
529
+ * Ctrl+C returns `exit`. Without a TTY there is no menu to read, so it fails
530
+ * open to `exit` and the caller leaves startup untouched.
531
+ */
532
+ function promptVersionAction(prompt) {
533
+ if (stdin.isTTY !== true) return Promise.resolve("exit");
534
+ const labels = [prompt.performLabel, prompt.cancelLabel];
535
+ return new Promise((resolve) => {
536
+ let selected = 0;
537
+ let drawn = 0;
538
+ const computeDrawn = (lines) => {
539
+ const cols = stdout.columns || 80;
540
+ return lines.reduce((total, ln) => {
541
+ if (ln === "") return total + 1;
542
+ const width = [...ln].reduce((sum, ch) => sum + (/^[\u1100-\u11ff\u2e80-\ua4cf\uf900-\ufaff\uff00-\uffef]/u.test(ch) ? 2 : 1), 0);
543
+ return total + Math.max(1, Math.ceil(width / cols));
544
+ }, 0);
545
+ };
546
+ const render = () => {
547
+ const lines = [
548
+ prompt.title,
549
+ prompt.message,
550
+ "",
551
+ ...labels.map((label, index) => index === selected ? `> ${label}` : ` ${label}`)
552
+ ];
553
+ if (drawn > 0) stdout.write(`\x1b[${drawn}A\x1b[0J`);
554
+ stdout.write(`${lines.join("\n")}\n`);
555
+ drawn = computeDrawn(lines);
556
+ };
557
+ const finish = (value) => {
558
+ if (stdin.isTTY === true) stdin.setRawMode(false);
559
+ stdin.pause();
560
+ stdin.off("keypress", onKeypress);
561
+ stdout.write("\n");
562
+ resolve(value);
563
+ };
564
+ const onKeypress = (_chunk, key) => {
565
+ if (key.ctrl && key.name === "c") {
566
+ finish("exit");
567
+ return;
568
+ }
569
+ if (key.name === "up") {
570
+ selected = selected === 0 ? labels.length - 1 : selected - 1;
571
+ render();
572
+ return;
573
+ }
574
+ if (key.name === "down") {
575
+ selected = selected === labels.length - 1 ? 0 : selected + 1;
576
+ render();
577
+ return;
578
+ }
579
+ if (key.name === "return") finish(selected === 0 ? "perform" : "exit");
580
+ };
581
+ emitKeypressEvents(stdin);
582
+ stdin.setRawMode(true);
583
+ stdin.resume();
584
+ stdin.on("keypress", onKeypress);
585
+ render();
586
+ });
587
+ }
588
+ //#endregion
589
+ //#region vendor/dsh-llm-pi-ai/src/auth.ts
590
+ /**
591
+ * Create private auth storage for adapters whose explicit source owns every credential.
592
+ * @returns an empty in-memory store and provider auth context, independent of ChatCode CLI login records.
593
+ */
594
+ function isolatedPiAiAuth() {
595
+ return {
596
+ credentials: new InMemoryCredentialStore(),
597
+ authContext: defaultProviderAuthContext()
598
+ };
599
+ }
600
+ //#endregion
601
+ //#region vendor/dsh-llm-pi-ai/src/replay.ts
602
+ /**
603
+ * Durable pi-ai replay metadata and assistant-history reconstruction.
604
+ *
605
+ * ChatCode CLI content remains the durable source for text and tool calls. This
606
+ * module stores only the provider-native metadata needed to reconstruct a
607
+ * pi-ai assistant message on a later request.
608
+ *
609
+ * @module dsh-llm-pi-ai/replay
610
+ */
611
+ /** Parse tool-call argument JSON; tolerate model malformations with {}. */
612
+ function parseArguments(raw) {
613
+ try {
614
+ const parsed = JSON.parse(raw);
615
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
616
+ } catch {}
617
+ return {};
618
+ }
619
+ /** Construct the zero usage value required by historical pi-ai messages. */
620
+ function emptyPiUsage() {
621
+ return {
622
+ input: 0,
623
+ output: 0,
624
+ cacheRead: 0,
625
+ cacheWrite: 0,
626
+ totalTokens: 0,
627
+ cost: {
628
+ input: 0,
629
+ output: 0,
630
+ cacheRead: 0,
631
+ cacheWrite: 0,
632
+ total: 0
633
+ }
634
+ };
635
+ }
636
+ /**
637
+ * Project a successful pi-ai response into the minimal durable replay state.
638
+ * The per-block half is index-aligned with the streamed blocks (pi-ai content
639
+ * order), so `BlockAssembler` prunes an entry with its block whenever assembly
640
+ * removes one.
641
+ * @param message - completed native pi-ai assistant response.
642
+ * @returns the versioned lossless-JSON replay projection.
643
+ */
644
+ function toPiReplayState(message) {
645
+ return {
646
+ response: {
647
+ kind: "pi-ai",
648
+ version: 2,
649
+ api: message.api,
650
+ provider: message.provider,
651
+ model: message.model,
652
+ ...message.responseModel === void 0 ? {} : { responseModel: message.responseModel },
653
+ ...message.responseId === void 0 ? {} : { responseId: message.responseId },
654
+ stopReason: message.stopReason
655
+ },
656
+ blocks: message.content.map((block) => {
657
+ switch (block.type) {
658
+ case "text": return {
659
+ type: "text",
660
+ ...block.textSignature === void 0 ? {} : { textSignature: block.textSignature }
661
+ };
662
+ case "thinking": return {
663
+ type: "reasoning",
664
+ ...block.thinkingSignature === void 0 ? {} : { thinkingSignature: block.thinkingSignature },
665
+ ...block.redacted === void 0 ? {} : { redacted: block.redacted }
666
+ };
667
+ case "toolCall": return {
668
+ type: "tool-call",
669
+ ...block.thoughtSignature === void 0 ? {} : { thoughtSignature: block.thoughtSignature }
670
+ };
671
+ }
672
+ })
673
+ };
674
+ }
675
+ function invalidReplay(message) {
676
+ throw new LlmError(`invalid pi-ai replay state: ${message}`, "INVALID_REPLAY_STATE");
677
+ }
678
+ /** Validate the durable adapter-private envelope before it reaches pi-ai. */
679
+ function readReplayState(value) {
680
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return invalidReplay("expected a replay envelope");
681
+ const envelope = value;
682
+ const rawResponse = envelope["response"];
683
+ if (typeof rawResponse !== "object" || rawResponse === null || Array.isArray(rawResponse)) return invalidReplay("expected a response object");
684
+ const response = rawResponse;
685
+ if (response["kind"] !== "pi-ai") return invalidReplay("unknown state kind");
686
+ if (response["version"] !== 2) return invalidReplay(`unsupported version ${String(response["version"])}`);
687
+ for (const key of [
688
+ "api",
689
+ "provider",
690
+ "model"
691
+ ]) if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
692
+ if (![
693
+ "stop",
694
+ "length",
695
+ "toolUse",
696
+ "error",
697
+ "aborted"
698
+ ].includes(String(response["stopReason"]))) return invalidReplay("unknown stopReason");
699
+ if (response["responseModel"] !== void 0 && typeof response["responseModel"] !== "string") return invalidReplay("responseModel must be a string");
700
+ if (response["responseId"] !== void 0 && typeof response["responseId"] !== "string") return invalidReplay("responseId must be a string");
701
+ const blocks = envelope["blocks"];
702
+ if (!Array.isArray(blocks)) return invalidReplay("blocks must be an array");
703
+ for (const [index, value] of blocks.entries()) {
704
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return invalidReplay(`block ${index} must be an object`);
705
+ const block = value;
706
+ if (![
707
+ "text",
708
+ "reasoning",
709
+ "tool-call"
710
+ ].includes(String(block["type"]))) return invalidReplay(`block ${index} has an unknown type`);
711
+ for (const signature of [
712
+ "textSignature",
713
+ "thinkingSignature",
714
+ "thoughtSignature"
715
+ ]) if (block[signature] !== void 0 && typeof block[signature] !== "string") return invalidReplay(`block ${index} ${signature} must be a string`);
716
+ if (block["redacted"] !== void 0 && typeof block["redacted"] !== "boolean") return invalidReplay(`block ${index} redacted must be boolean`);
717
+ }
718
+ return {
719
+ response,
720
+ blocks
721
+ };
722
+ }
723
+ /** Convert provider-neutral blocks without trusting them as same-model replay. */
724
+ function foreignAssistant(message) {
725
+ const source = message.source.kind === "model" ? message.source : void 0;
726
+ const content = [];
727
+ for (const block of message.content) switch (block.type) {
728
+ case "text":
729
+ content.push({
730
+ type: "text",
731
+ text: block.text
732
+ });
733
+ break;
734
+ case "reasoning":
735
+ content.push({
736
+ type: "thinking",
737
+ thinking: block.text
738
+ });
739
+ break;
740
+ case "tool-call":
741
+ content.push({
742
+ type: "toolCall",
743
+ id: block.id,
744
+ name: block.name,
745
+ arguments: parseArguments(block.arguments)
746
+ });
747
+ break;
748
+ case "image": throw new LlmError("pi-ai chat history cannot represent structured assistant image output", "UNSUPPORTED_CONTENT");
749
+ }
750
+ return {
751
+ role: "assistant",
752
+ content,
753
+ api: "dsh-foreign",
754
+ provider: source?.provider ?? "dsh-foreign",
755
+ model: source?.model ?? "dsh-foreign",
756
+ usage: emptyPiUsage(),
757
+ stopReason: content.some((piece) => piece.type === "toolCall") ? "toolUse" : "stop",
758
+ timestamp: 0
759
+ };
760
+ }
761
+ /** Recombine durable ChatCode CLI content with validated pi-ai replay metadata. */
762
+ function replayedAssistant(message, source, rawState) {
763
+ const state = readReplayState(rawState);
764
+ if (state.response.provider !== source.provider) return invalidReplay("provider does not match assistant source");
765
+ if (state.response.model !== source.model) return invalidReplay("model does not match assistant source");
766
+ if (state.blocks.length !== message.content.length) return invalidReplay("block count does not match assistant content");
767
+ return {
768
+ role: "assistant",
769
+ content: message.content.map((block, index) => {
770
+ const replay = state.blocks[index];
771
+ if (replay === void 0 || replay.type !== block.type) return invalidReplay(`block ${index} does not match assistant content`);
772
+ switch (block.type) {
773
+ case "text": return {
774
+ type: "text",
775
+ text: block.text,
776
+ ...replay.type === "text" && replay.textSignature !== void 0 ? { textSignature: replay.textSignature } : {}
777
+ };
778
+ case "reasoning": return {
779
+ type: "thinking",
780
+ thinking: block.text,
781
+ ...replay.type === "reasoning" && replay.thinkingSignature !== void 0 ? { thinkingSignature: replay.thinkingSignature } : {},
782
+ ...replay.type === "reasoning" && replay.redacted !== void 0 ? { redacted: replay.redacted } : {}
783
+ };
784
+ case "tool-call": return {
785
+ type: "toolCall",
786
+ id: block.id,
787
+ name: block.name,
788
+ arguments: parseArguments(block.arguments),
789
+ ...replay.type === "tool-call" && replay.thoughtSignature !== void 0 ? { thoughtSignature: replay.thoughtSignature } : {}
790
+ };
791
+ /* v8 ignore next -- readReplayState rejects unknown replay tags, so an equal plugin-added ChatCode CLI tag cannot reach this switch */
792
+ default: return invalidReplay(`block ${index} has an unsupported ChatCode CLI type`);
793
+ }
794
+ }),
795
+ api: state.response.api,
796
+ provider: state.response.provider,
797
+ model: state.response.model,
798
+ ...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
799
+ ...state.response.responseId === void 0 ? {} : { responseId: state.response.responseId },
800
+ usage: emptyPiUsage(),
801
+ stopReason: state.response.stopReason,
802
+ timestamp: 0
803
+ };
804
+ }
805
+ /**
806
+ * Convert one durable ChatCode CLI assistant message into pi-ai history.
807
+ *
808
+ * Durable content is the authoritative record; replay metadata only restores
809
+ * native fidelity (ids, signatures). A replay state this build cannot use —
810
+ * another adapter's kind, another version, a malformed value, or metadata that
811
+ * no longer matches the content — therefore degrades the one message to
812
+ * provider-neutral history instead of failing the request.
813
+ * @param message - assistant content with required source and optional adapter-owned replay metadata.
814
+ * @param onDegrade - called with the diagnostic reason when an unusable replay
815
+ * state falls back to provider-neutral conversion.
816
+ * @returns a native pi-ai assistant message reconstructed from durable content.
817
+ */
818
+ function toPiAssistant(message, onDegrade) {
819
+ const source = message.source;
820
+ if (source.kind !== "model" || source.replayState === void 0) return foreignAssistant(message);
821
+ try {
822
+ return replayedAssistant(message, source, source.replayState);
823
+ } catch (error) {
824
+ /* v8 ignore next -- replayedAssistant throws only INVALID_REPLAY_STATE LlmErrors; the
825
+ guard keeps a future non-replay failure loud instead of silently degrading it */
826
+ if (!(error instanceof LlmError) || error.code !== "INVALID_REPLAY_STATE") throw error;
827
+ onDegrade?.(error.message);
828
+ return foreignAssistant(message);
829
+ }
830
+ }
831
+ //#endregion
832
+ //#region vendor/dsh-llm-pi-ai/src/catalog.ts
833
+ /**
834
+ * Materialization of one provider route's model catalog. The installed pi-ai
835
+ * catalog supplies defaults keyed by model id, and a profile's own model
836
+ * entries override them field by field, so a route naming a catalog provider
837
+ * stays configuration-free while a route pi-ai has never heard of is fully
838
+ * describable from `settings.yaml`.
839
+ *
840
+ * Every pi-ai `Model` field the harness cannot default is required here rather
841
+ * than at request time: an unserviceable route fails while its configuration is
842
+ * being resolved, which is the earliest point that can name the offending key.
843
+ *
844
+ * @module dsh-llm-pi-ai/catalog
845
+ */
846
+ /**
847
+ * Pricing for a model the installed catalog does not describe. The harness
848
+ * never reads pi-ai's cost metadata — `replay.ts` zeroes it and no consumer
849
+ * reports spend — so this is the absence of a fact, not a configurable rate.
850
+ */
851
+ const NO_COST = {
852
+ input: 0,
853
+ output: 0,
854
+ cacheRead: 0,
855
+ cacheWrite: 0
856
+ };
857
+ /** Every request modality a profile may declare. */
858
+ const MODALITIES = Object.keys({
859
+ text: true,
860
+ image: true
861
+ });
862
+ /**
863
+ * One entry's modality list, or `undefined` when it states no answer. Absent
864
+ * and empty mean the same thing — `[]` describes a model that accepts nothing
865
+ * and could serve no request — which is what makes an entry naming a catalog
866
+ * model without declaring modalities keep the catalog's, since the config
867
+ * schema materializes `[]` for an absent array.
868
+ * @param configured - the list a `models` or `modelOverrides` entry supplied.
869
+ * @returns the declared modalities, or `undefined` to ask the next level.
870
+ */
871
+ function declaredInput(configured) {
872
+ return configured === void 0 || configured.length === 0 ? void 0 : [...configured];
873
+ }
874
+ /** Every pi-ai thinking level a profile may declare, in escalation order. */
875
+ const THINKING_LEVELS = Object.keys({
876
+ off: true,
877
+ minimal: true,
878
+ low: true,
879
+ medium: true,
880
+ high: true,
881
+ xhigh: true,
882
+ max: true
883
+ });
884
+ /** Reasoning-dispatch wire formats a profile may name, most-reached first. */
885
+ const SUPPORTED_THINKING_FORMATS = Object.keys({
886
+ "openai": true,
887
+ "deepseek": true,
888
+ "openrouter": true,
889
+ "together": true,
890
+ "baseten": true,
891
+ "zai": true,
892
+ "qwen": true,
893
+ "chat-template": true,
894
+ "qwen-chat-template": true,
895
+ "string-thinking": true,
896
+ "ant-ling": true
897
+ });
898
+ /** The output-cap field spellings a profile may name. */
899
+ const MAX_TOKENS_FIELDS = Object.keys({
900
+ max_completion_tokens: true,
901
+ max_tokens: true
902
+ });
903
+ /** The prompt-cache marker conventions a profile may name. */
904
+ const CACHE_CONTROL_FORMATS = Object.keys({ anthropic: true });
905
+ /** The request-state placeholders a profile may name. */
906
+ const CHAT_TEMPLATE_VARS = Object.keys({
907
+ "thinking.enabled": true,
908
+ "thinking.effort": true
909
+ });
910
+ let providerIndex;
911
+ /**
912
+ * Installed catalog providers by id, constructed once. Each entry owns the API
913
+ * implementations for its own models, which is why a catalog route reuses this
914
+ * provider instead of being rebuilt from parts.
915
+ * @returns the catalog provider index.
916
+ */
917
+ function catalogProviders() {
918
+ providerIndex ??= new Map(builtinProviders().map((provider) => [provider.id, provider]));
919
+ return providerIndex;
920
+ }
921
+ /**
922
+ * The installed catalog provider for one route, when pi-ai ships one.
923
+ * @param provider - provider route key.
924
+ * @returns the catalog provider, or `undefined` for a route pi-ai does not ship.
925
+ */
926
+ function catalogProvider(provider) {
927
+ return catalogProviders().get(provider);
928
+ }
929
+ /**
930
+ * The installed catalog models for one route, indexed by model id.
931
+ * @param provider - provider route key.
932
+ * @returns catalog models by id; empty for a route pi-ai does not ship.
933
+ */
934
+ function catalogModels(provider) {
935
+ if (!catalogProviders().has(provider)) return /* @__PURE__ */ new Map();
936
+ const models = getBuiltinModels(provider);
937
+ return new Map(models.map((model) => [model.id, model]));
938
+ }
939
+ /**
940
+ * Disposition of every `OpenAICompletionsCompat` field. The `Record` key type
941
+ * is a drift gate: a pi-ai upgrade that adds a field fails compilation here
942
+ * until it is classified, so the offer never silently lags the upstream set.
943
+ */
944
+ const COMPLETIONS_COMPAT_GATE = {
945
+ supportsStore: "offer",
946
+ supportsDeveloperRole: "offer",
947
+ supportsReasoningEffort: "offer",
948
+ supportsUsageInStreaming: "offer",
949
+ supportsFinishReason: "offer",
950
+ maxTokensField: "offer",
951
+ requiresToolResultName: "offer",
952
+ requiresAssistantAfterToolResult: "offer",
953
+ requiresThinkingAsText: "offer",
954
+ requiresReasoningContentOnAssistantMessages: "offer",
955
+ thinkingFormat: "offer",
956
+ chatTemplateKwargs: "offer",
957
+ chatTemplateArgs: "offer",
958
+ supportsThinkingTokenBudget: "offer",
959
+ supportsStrictMode: "offer",
960
+ cacheControlFormat: "offer",
961
+ supportsLongCacheRetention: "offer",
962
+ openRouterRouting: "withhold",
963
+ vercelGatewayRouting: "withhold",
964
+ zaiToolStream: "withhold",
965
+ supportsOpenAIGrammarTools: "withhold",
966
+ sendSessionAffinityHeaders: "withhold",
967
+ deferredToolsMode: "withhold",
968
+ sessionAffinityFormat: "withhold"
969
+ };
970
+ /** Disposition of every `OpenAIResponsesCompat` field; a drift gate like the one above. */
971
+ const RESPONSES_COMPAT_GATE = {
972
+ supportsDeveloperRole: "offer",
973
+ supportsStrictMode: "offer",
974
+ supportsLongCacheRetention: "offer",
975
+ sessionAffinityFormat: "withhold",
976
+ supportsOpenAIGrammarTools: "withhold",
977
+ supportsAdditionalTools: "withhold",
978
+ supportsToolSearch: "withhold",
979
+ supportsExplicitPromptCacheMode: "withhold"
980
+ };
981
+ /**
982
+ * The compat gate of every wire protocol a profile may configure.
983
+ *
984
+ * Keyed by protocol, but grouped by pi-ai's compat *type*: the three Responses
985
+ * protocols share `OpenAIResponsesCompat`, so a switch settable on one is
986
+ * settable on all three. Keying by protocol alone would refuse
987
+ * `azure-openai-responses` and `openai-codex-responses` the fields their own
988
+ * models declare.
989
+ */
990
+ const COMPAT_GATES = {
991
+ "openai-completions": COMPLETIONS_COMPAT_GATE,
992
+ "openai-responses": RESPONSES_COMPAT_GATE,
993
+ "azure-openai-responses": RESPONSES_COMPAT_GATE,
994
+ "openai-codex-responses": RESPONSES_COMPAT_GATE,
995
+ "anthropic-messages": {
996
+ supportsEagerToolInputStreaming: "offer",
997
+ supportsLongCacheRetention: "offer",
998
+ supportsCacheControlOnTools: "offer",
999
+ supportsTemperature: "offer",
1000
+ forceAdaptiveThinking: "offer",
1001
+ allowEmptySignature: "offer",
1002
+ supportsStrictTools: "offer",
1003
+ sendSessionAffinityHeaders: "withhold",
1004
+ supportsToolReferences: "withhold"
1005
+ },
1006
+ "bedrock-converse-stream": { supportsStrictMode: "offer" }
1007
+ };
1008
+ /**
1009
+ * The compat gate of one resolved protocol. A `string` lookup rather than a
1010
+ * keyed read: a route's `api` is configuration, so it may name a protocol
1011
+ * pi-ai gives no compat type — or none at all.
1012
+ * @param api - resolved wire protocol.
1013
+ * @returns that protocol's field gate, or `undefined` when it takes no compat.
1014
+ */
1015
+ function compatGate(api) {
1016
+ return COMPAT_GATES[api];
1017
+ }
1018
+ /**
1019
+ * The compat entries a profile actually set.
1020
+ *
1021
+ * schemastery materializes an absent dict as `{}` — the behavior
1022
+ * `reasoningEfforts` works around with a union — so every parsed profile
1023
+ * carries both template-argument keys whether or not anyone wrote them. An
1024
+ * empty one states nothing here: it would send no arguments, which is exactly
1025
+ * what leaving the field out does, so absent and empty are the same request
1026
+ * and neither may make a route look like it configured a switch. A valueless
1027
+ * scalar is the other thing schemastery lets through, and it is refused by
1028
+ * {@link assertOfferedCompatFields} before this runs rather than filtered.
1029
+ * @param compat - the configured switches, when any.
1030
+ * @returns the entries carrying a value, in declaration order.
1031
+ */
1032
+ function configuredCompatEntries(compat) {
1033
+ return Object.entries(compat ?? {}).flatMap(([field, value]) => {
1034
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0 ? [] : [[field, value]];
1035
+ });
1036
+ }
1037
+ /**
1038
+ * The protocols offering one compat field, in {@link COMPAT_GATES} order.
1039
+ * @param field - configured compat field name.
1040
+ * @returns the protocols whose compat takes it; empty when none does, which
1041
+ * is either a withheld field or a name no upstream compat type declares.
1042
+ */
1043
+ function compatProtocols(field) {
1044
+ return Object.entries(COMPAT_GATES).flatMap(([api, gate]) => gate[field] === "offer" ? [api] : []);
1045
+ }
1046
+ /**
1047
+ * The compat fields one protocol offers, for a diagnostic that has to show
1048
+ * what was available instead of the name that missed.
1049
+ * @param api - wire protocol.
1050
+ * @returns the offered field names, or an empty list for a protocol taking no compat.
1051
+ */
1052
+ function offeredCompatFields(api) {
1053
+ return Object.entries(compatGate(api) ?? {}).flatMap(([field, disposition]) => disposition === "offer" ? [field] : []);
1054
+ }
1055
+ /**
1056
+ * Every offered field name, deduplicated, for the one diagnostic that cannot
1057
+ * narrow by protocol: the vocabulary check runs before any protocol resolves,
1058
+ * which is what lets it refuse a misspelling on a route whose models would
1059
+ * never have reached the protocol that declares the intended field.
1060
+ * @returns the offered field names across every protocol, in gate order.
1061
+ */
1062
+ function allOfferedCompatFields() {
1063
+ const fields = /* @__PURE__ */ new Set();
1064
+ for (const api of Object.keys(COMPAT_GATES)) for (const field of offeredCompatFields(api)) fields.add(field);
1065
+ return [...fields];
1066
+ }
1067
+ /**
1068
+ * Reject a compat key no protocol offers. Runs before any protocol is
1069
+ * resolved, so a withheld field or a misspelling fails even on a route whose
1070
+ * models never reach the protocol that would have taken it — the alternative
1071
+ * being the silent drop that let an unreadable switch look applied.
1072
+ * @param provider - provider route key, for diagnostics.
1073
+ * @param site - the configuration site, for diagnostics.
1074
+ * @param compat - the configured switches, when any.
1075
+ * @throws Error naming the offending key.
1076
+ */
1077
+ function assertOfferedCompatFields(provider, site, compat) {
1078
+ for (const [field, value] of Object.entries(compat ?? {})) {
1079
+ if (compatProtocols(field).length === 0) {
1080
+ if (Object.values(COMPAT_GATES).some((gate) => gate[field] !== void 0)) invalid$1(provider, `${site} sets compat "${field}", which is not configurable here: pi-ai's installed catalog sets it for the vendors that need it, so name that provider as the route instead`);
1081
+ invalid$1(provider, `${site} sets compat "${field}", which no wire protocol declares; the configurable switches are ${allOfferedCompatFields().join(", ")}`);
1082
+ }
1083
+ if (value == null) invalid$1(provider, `${site} sets compat "${field}" with no value; give it one, or remove the key to leave the field to the next layer — the installed catalog entry, then pi-ai's own detection`);
1084
+ }
1085
+ }
1086
+ /** Report a route the deployment cannot serve, naming the settings key at fault. */
1087
+ function invalid$1(provider, detail) {
1088
+ throw new Error(`llm-pi-ai: provider "${provider}" ${detail}`);
1089
+ }
1090
+ /**
1091
+ * The one wire protocol a catalog route's shipped models agree on. This is what
1092
+ * lets a deployment add a model the installed catalog has not caught up with —
1093
+ * a provider's newest release — without restating the protocol its siblings
1094
+ * already use. A route whose shipped models disagree (an OpenAI-style catalog
1095
+ * spanning Responses and Chat Completions) has no such answer, so a model it
1096
+ * does not describe must name its protocol at the route.
1097
+ */
1098
+ function sharedCatalogApi(defaults) {
1099
+ const apis = /* @__PURE__ */ new Set();
1100
+ for (const model of defaults.values()) apis.add(model.api);
1101
+ return apis.size === 1 ? [...apis][0] : void 0;
1102
+ }
1103
+ /**
1104
+ * Resolve one model's reasoning capability from its declared efforts.
1105
+ *
1106
+ * A declared dict translates to pi-ai's `thinkingLevelMap` with every level
1107
+ * decided explicitly: declared levels carry their wire spelling, undeclared
1108
+ * levels are pinned to `null` (unsupported). Pinning matters because pi-ai's
1109
+ * own defaulting is asymmetric — an absent key means "supported" for the five
1110
+ * base levels but "unsupported" for `xhigh`/`max` — and a profile author
1111
+ * should not need to know that. A declared `off` with no value is the one
1112
+ * exception: it stays absent from the map, which pi-ai reads as "supported,
1113
+ * send nothing" — the correct dispatch where not thinking is the parameter's
1114
+ * absence — while `off` with a value sends that value.
1115
+ * @param provider - provider route key, for diagnostics.
1116
+ * @param entry - the configured model entry.
1117
+ * @param base - the installed catalog entry of the same id, when one exists.
1118
+ * @returns the reasoning fields the materialized model carries.
1119
+ */
1120
+ function resolveModelReasoning(provider, entry, base) {
1121
+ const efforts = entry.reasoningEfforts;
1122
+ if (efforts === void 0) return { reasoning: base?.reasoning ?? false };
1123
+ if (efforts === false) return { reasoning: false };
1124
+ if (efforts === null || Object.keys(efforts).length === 0) invalid$1(provider, `model "${entry.id}" has an empty reasoningEfforts; declare the offered levels, set false for a non-reasoning model, or omit the field to keep the installed catalog's capability`);
1125
+ const declared = THINKING_LEVELS.flatMap((level) => {
1126
+ const wire = efforts[level];
1127
+ return wire === void 0 ? [] : [[level, wire]];
1128
+ });
1129
+ for (const [level, wire] of declared) if (wire === null) {
1130
+ if (level !== "off") invalid$1(provider, `model "${entry.id}" reasoningEfforts.${level} needs the wire value dispatch should send; only "off" may leave it empty`);
1131
+ } else if (wire.length === 0) invalid$1(provider, `model "${entry.id}" reasoningEfforts.${level} must not be an empty string`);
1132
+ if (!declared.some(([level]) => level !== "off")) invalid$1(provider, `model "${entry.id}" reasoningEfforts offers no level beyond "off"; declare a thinking level, or set reasoningEfforts to false for a non-reasoning model`);
1133
+ const map = {};
1134
+ for (const level of THINKING_LEVELS) {
1135
+ const wire = efforts[level];
1136
+ if (wire === void 0) map[level] = null;
1137
+ else if (wire !== null) map[level] = wire;
1138
+ }
1139
+ return {
1140
+ reasoning: true,
1141
+ thinkingLevelMap: map
1142
+ };
1143
+ }
1144
+ /**
1145
+ * Resolve one model's compat block from the profile's switches.
1146
+ *
1147
+ * A model switch wins over the route switch field by field; whatever neither
1148
+ * sets keeps the installed entry's value, and a field no layer decides falls
1149
+ * through to pi-ai's own detection. A model-level switch its protocol does not
1150
+ * take fails resolution — about one named model it can only be a mistake —
1151
+ * while a route-level one skips past such models, since a route default must
1152
+ * stay settable on a route whose models do not all speak one protocol. Every
1153
+ * field reaching here is offered by some protocol; {@link
1154
+ * assertOfferedCompatFields} has already refused the rest.
1155
+ * @param provider - provider route key, for diagnostics.
1156
+ * @param entry - the configured model entry.
1157
+ * @param route - the route-level switches, when any.
1158
+ * @param base - the installed catalog entry of the same id, when one exists.
1159
+ * @param api - the model's resolved wire protocol.
1160
+ * @returns a `compat` field to spread into the model, or nothing.
1161
+ */
1162
+ function resolveModelCompat(provider, entry, route, base, api) {
1163
+ const gate = compatGate(api);
1164
+ const configured = {};
1165
+ for (const [field, value] of configuredCompatEntries(route)) {
1166
+ if (gate?.[field] !== "offer") continue;
1167
+ configured[field] = value;
1168
+ }
1169
+ for (const [field, value] of configuredCompatEntries(entry.compat)) {
1170
+ if (gate?.[field] !== "offer") {
1171
+ const offered = offeredCompatFields(api);
1172
+ invalid$1(provider, `model "${entry.id}" sets compat "${field}", but its api is "${api}", which does not take it; that switch exists on ${compatProtocols(field).join(", ")}, and "${api}" offers ${offered.length === 0 ? "no configurable compat" : offered.join(", ")}`);
1173
+ }
1174
+ configured[field] = value;
1175
+ }
1176
+ if (Object.keys(configured).length === 0) return {};
1177
+ return { compat: {
1178
+ ...base?.api === api ? base.compat : void 0,
1179
+ ...configured
1180
+ } };
1181
+ }
1182
+ /**
1183
+ * Materialize one route's catalog by merging the installed catalog defaults
1184
+ * under the configured entries. A route with no configured `models` serves the
1185
+ * installed catalog unchanged, which is what keeps an existing
1186
+ * `providers: { deepseek: { apiKeyEnv: … } }` profile working untouched.
1187
+ * @param request - the route-level catalog facts.
1188
+ * @returns the materialized models and the explicitly configured request caps.
1189
+ */
1190
+ function resolveRouteModels(request) {
1191
+ const { provider } = request;
1192
+ const defaults = catalogModels(provider);
1193
+ const providerBaseUrl = catalogProvider(provider)?.baseUrl;
1194
+ const configured = request.models ?? [];
1195
+ const overrides = request.modelOverrides ?? {};
1196
+ for (const [id, override] of Object.entries(overrides)) {
1197
+ if (id.length === 0) invalid$1(provider, "has a modelOverrides entry with an empty model id");
1198
+ if (defaults.size === 0) invalid$1(provider, `sets modelOverrides for "${id}", but the installed catalog does not describe this route; a declared route spells every model out in its models list`);
1199
+ if (configured.length > 0) invalid$1(provider, `sets modelOverrides for "${id}" beside a models list; models already replaces the served catalog, so declare the fields on its entries`);
1200
+ if (!defaults.has(id)) invalid$1(provider, `modelOverrides names "${id}", which the installed catalog does not describe`);
1201
+ if ("id" in override) invalid$1(provider, `modelOverrides entry "${id}" sets "id", which is the dict key`);
1202
+ }
1203
+ const entries = configured.length > 0 ? configured : [...defaults.values()].map((model) => ({
1204
+ id: model.id,
1205
+ ...overrides[model.id]
1206
+ }));
1207
+ if (entries.length === 0) invalid$1(provider, "resolves no models; the installed catalog does not describe this route, so its models must be listed in configuration");
1208
+ const routeApi = sharedCatalogApi(defaults);
1209
+ assertOfferedCompatFields(provider, "route", request.compat);
1210
+ for (const entry of entries) assertOfferedCompatFields(provider, `model "${entry.id}"`, entry.compat);
1211
+ const seen = /* @__PURE__ */ new Set();
1212
+ const configuredMaxTokens = /* @__PURE__ */ new Map();
1213
+ const models = entries.map((entry) => {
1214
+ if (entry.id.length === 0) invalid$1(provider, "has a model with an empty id");
1215
+ if (seen.has(entry.id)) invalid$1(provider, `lists model "${entry.id}" more than once`);
1216
+ seen.add(entry.id);
1217
+ const base = defaults.get(entry.id);
1218
+ const api = request.api ?? base?.api ?? routeApi;
1219
+ if (api === void 0) invalid$1(provider, `model "${entry.id}" needs an api; the installed catalog does not describe it, so set the route's api to the wire protocol its endpoint speaks`);
1220
+ const baseUrl = request.baseURL ?? base?.baseUrl ?? providerBaseUrl;
1221
+ if (baseUrl === void 0) invalid$1(provider, `model "${entry.id}" needs a baseURL; the installed catalog does not describe this route`);
1222
+ const contextWindow = entry.contextWindow ?? base?.contextWindow ?? request.defaultContextWindow;
1223
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) invalid$1(provider, `model "${entry.id}" contextWindow must be a positive integer`);
1224
+ const maxTokens = entry.maxTokens ?? base?.maxTokens ?? request.defaultMaxTokens;
1225
+ if (!Number.isInteger(maxTokens) || maxTokens <= 0) invalid$1(provider, `model "${entry.id}" maxTokens must be a positive integer`);
1226
+ if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
1227
+ return {
1228
+ ...base,
1229
+ id: entry.id,
1230
+ name: entry.name ?? base?.name ?? entry.id,
1231
+ api,
1232
+ provider,
1233
+ baseUrl,
1234
+ input: declaredInput(entry.input) ?? base?.input ?? [...request.defaultInput],
1235
+ cost: base?.cost ?? NO_COST,
1236
+ contextWindow,
1237
+ maxTokens,
1238
+ ...resolveModelReasoning(provider, entry, base),
1239
+ ...resolveModelCompat(provider, entry, request.compat, base, api)
1240
+ };
1241
+ });
1242
+ for (const [field] of configuredCompatEntries(request.compat)) {
1243
+ const takers = compatProtocols(field);
1244
+ if (models.some((model) => takers.includes(model.api))) continue;
1245
+ invalid$1(provider, `sets compat "${field}", but no model on the route speaks a protocol that takes it; it exists on ${takers.join(", ")}`);
1246
+ }
1247
+ return {
1248
+ models,
1249
+ configuredMaxTokens
1250
+ };
1251
+ }
1252
+ //#endregion
1253
+ //#region vendor/dsh-llm-pi-ai/src/provider.ts
1254
+ /**
1255
+ * Construction of the pi-ai `Provider` that one configured route registers into
1256
+ * the adapter's `Models` collection.
1257
+ *
1258
+ * Two constructions, one decision: a route the installed catalog ships, whose
1259
+ * profile does not override the wire protocol, **reuses that catalog provider**
1260
+ * with its models replaced — the catalog provider owns API implementations this
1261
+ * package cannot reconstruct (Bedrock loads its Smithy module through a
1262
+ * separate entry point), so rebuilding it from parts would silently narrow
1263
+ * which providers work. Every other route — one pi-ai has never heard of, or a
1264
+ * catalog route pointed at a different protocol — is built by `createProvider`
1265
+ * over the protocol table below.
1266
+ *
1267
+ * Credentials never reach this module's storage: the harness resolves a route's
1268
+ * key through `ctx.credentials` before the request enters pi-ai and hands it
1269
+ * over as a stream option, which `Models` presents to `resolve()` as the
1270
+ * credential key.
1271
+ *
1272
+ * @module dsh-llm-pi-ai/provider
1273
+ */
1274
+ /**
1275
+ * Wire protocols a configured route may name, mapped to pi-ai's lazily loaded
1276
+ * implementations. Each entry is the factory that pi-ai's matching provider
1277
+ * factory uses, so a hand-declared route reaches exactly the implementation a
1278
+ * catalog route would.
1279
+ *
1280
+ * The table is deliberately narrow: the protocols a hand-declared route
1281
+ * actually reads, each completely describable with a key, an
1282
+ * endpoint, and headers. Bedrock signs with SigV4 over AWS credentials and a
1283
+ * region, Vertex needs a project, a location, and application-default
1284
+ * credentials, Azure needs provider environment plus an api-version, and Codex
1285
+ * authenticates through OAuth — none of which this configuration shape can
1286
+ * express, so offering them would hand back a provider that cannot
1287
+ * authenticate. The remainder are absent for want of a consumer rather than a
1288
+ * blocker: each is one line here once a deployment needs it. Catalog routes
1289
+ * still reach every protocol through their own provider; only an explicit
1290
+ * override is refused.
1291
+ */
1292
+ const PROTOCOLS = {
1293
+ "openai-completions": openAICompletionsApi,
1294
+ "openai-responses": openAIResponsesApi,
1295
+ "anthropic-messages": anthropicMessagesApi
1296
+ };
1297
+ /**
1298
+ * Every wire protocol a configured route may name, most-reached first. The
1299
+ * order is the table's and therefore stable; a configuration surface offering
1300
+ * a choice presents the first as its default, which is why the protocol a
1301
+ * hand-declared gateway most often speaks — and the one endpoint interrogation
1302
+ * can read — leads.
1303
+ * @returns the supported protocol identifiers.
1304
+ */
1305
+ function supportedProtocols() {
1306
+ return Object.keys(PROTOCOLS);
1307
+ }
1308
+ /**
1309
+ * Api-key auth for a route the harness authenticates itself. `Models` calls
1310
+ * this after the adapter has already resolved the route's credential, so a
1311
+ * missing key here is not this layer's failure: a named-but-unresolvable
1312
+ * reference has already failed the request with `MISSING_CREDENTIAL`, and a
1313
+ * route naming no credential at all is deliberately unauthenticated. Reporting
1314
+ * it as configured hands the decision to the protocol, which is where the
1315
+ * requirement actually lives — pi-ai's OpenAI-compatible implementation, for
1316
+ * one, still insists on a key or an `Authorization` header of its own.
1317
+ * @param name - display name used as the resolution's status label.
1318
+ * @returns the api-key auth for a harness-authenticated route.
1319
+ */
1320
+ function harnessApiKeyAuth(name) {
1321
+ return {
1322
+ name,
1323
+ resolve: ({ credential }) => Promise.resolve({
1324
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
1325
+ source: name
1326
+ })
1327
+ };
1328
+ }
1329
+ /**
1330
+ * The auth one route resolves its credential through.
1331
+ *
1332
+ * A catalog route keeps the installed provider's own auth, which is what
1333
+ * preserves provider-native ambient discovery for a profile naming no
1334
+ * credential. That holds even when the profile repoints the protocol: which
1335
+ * environment a provider reads is a property of the provider, not of the wire
1336
+ * format its models speak.
1337
+ *
1338
+ * The single addition covers a catalog provider that offers no api-key method
1339
+ * at all. pi-ai resolves a request's `apiKey` override only when the provider
1340
+ * declares one (`resolveProviderAuth` checks `provider.auth.apiKey` before
1341
+ * honouring the override), so an OAuth-only provider — `openai-codex` is the
1342
+ * one the installed catalog ships — would refuse a profile's explicit key with
1343
+ * `Provider is not configured` before any request went out. Adding the harness
1344
+ * method beside the provider's own restores that route. A keyless profile adds
1345
+ * nothing and still reports the honest refusal, because this adapter resolves
1346
+ * credentials through its own seam and holds no OAuth store to fall back on.
1347
+ * @param spec - the resolved route facts.
1348
+ * @param catalog - the installed catalog provider, when pi-ai ships one.
1349
+ * @returns the auth to construct this route's provider with.
1350
+ */
1351
+ function routeAuth(spec, catalog) {
1352
+ if (catalog === void 0) return { apiKey: harnessApiKeyAuth(spec.displayName) };
1353
+ if (catalog.auth.apiKey !== void 0 || !spec.namesCredential) return catalog.auth;
1354
+ return {
1355
+ ...catalog.auth,
1356
+ apiKey: harnessApiKeyAuth(spec.displayName)
1357
+ };
1358
+ }
1359
+ /**
1360
+ * Reuse an installed catalog provider with this route's models and identity.
1361
+ * Model dispatch stays with the catalog provider, so its API implementations,
1362
+ * compatibility quirks, and ambient credential discovery are preserved exactly.
1363
+ * Catalog-owned dynamic refresh is dropped: this route's catalog is the
1364
+ * settings document, and a background refresh would contradict it.
1365
+ */
1366
+ function reuseCatalogProvider(base, spec) {
1367
+ const baseUrl = spec.baseURL ?? base.baseUrl;
1368
+ return {
1369
+ id: spec.provider,
1370
+ name: spec.displayName,
1371
+ ...baseUrl === void 0 ? {} : { baseUrl },
1372
+ auth: routeAuth(spec, base),
1373
+ getModels: () => spec.models,
1374
+ stream: (model, context, options) => base.stream(model, context, options),
1375
+ streamSimple: (model, context, options) => base.streamSimple(model, context, options)
1376
+ };
1377
+ }
1378
+ /**
1379
+ * Build the pi-ai provider for one resolved route.
1380
+ * @param spec - the resolved route facts.
1381
+ * @returns the provider to register in the adapter's `Models` collection.
1382
+ * @throws Error when the route names a wire protocol this build cannot serve.
1383
+ */
1384
+ function buildProvider(spec) {
1385
+ const catalog = catalogProvider(spec.provider);
1386
+ if (catalog !== void 0 && spec.api === void 0) return reuseCatalogProvider(catalog, spec);
1387
+ const factory = spec.api === void 0 ? void 0 : PROTOCOLS[spec.api];
1388
+ if (factory === void 0) throw new Error(`llm-pi-ai: provider "${spec.provider}" names api "${spec.api}", which this build cannot serve; supported protocols are ${supportedProtocols().join(", ")}`);
1389
+ return createProvider({
1390
+ id: spec.provider,
1391
+ name: spec.displayName,
1392
+ ...spec.baseURL === void 0 ? {} : { baseUrl: spec.baseURL },
1393
+ auth: routeAuth(spec, catalog),
1394
+ models: spec.models,
1395
+ api: factory()
1396
+ });
1397
+ }
1398
+ //#endregion
1399
+ //#region vendor/dsh-llm-pi-ai/src/config.ts
1400
+ /** Default maximum idle interval while an adapter stream read is outstanding. */
1401
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1402
+ /**
1403
+ * Default request-level bound on base64-encoded image payload. Every image in
1404
+ * history is re-encoded into every request body, so an unbounded conversation
1405
+ * eventually exceeds a provider or gateway request-size cap and the session
1406
+ * can never complete another request. The 20MiB default admits fifteen 1MiB
1407
+ * request versions after base64 expansion and reserves request capacity for
1408
+ * system prompts, history, tools, and JSON.
1409
+ * Deployments behind stricter gateways lower it per route.
1410
+ */
1411
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20971520;
1412
+ /** Default total-pixel budget preserves the complete 2048px normalized attachment. */
1413
+ const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
1414
+ /** Default raw encoded-byte target before inline base64 expansion; the smallest quality-ladder output is used when no quality fits. */
1415
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1048576;
1416
+ /** Context capacity assumed for a model neither configuration nor the catalog sizes. */
1417
+ const DEFAULT_CONTEXT_WINDOW = 262144;
1418
+ /** Output capability assumed for a model neither configuration nor the catalog sizes. */
1419
+ const DEFAULT_MAX_TOKENS = 32768;
1420
+ /**
1421
+ * Modalities assumed for a model neither configuration nor the catalog
1422
+ * declares. Text is the floor every supported protocol certainly carries, so
1423
+ * this is the absence of a declaration rather than a guess at the endpoint:
1424
+ * nothing can interrogate a gateway for its modalities, and the two wrong
1425
+ * answers do not cost the same. Under-claiming refuses the image before it is
1426
+ * attached, naming the model. Over-claiming admits one the provider then
1427
+ * rejects mid-turn, after the message is durable, leaving the session
1428
+ * repeating a request that cannot succeed.
1429
+ */
1430
+ const DEFAULT_INPUT = ["text"];
1431
+ const thinkingBudgets = z.object({
1432
+ minimal: z.number(),
1433
+ low: z.number(),
1434
+ medium: z.number(),
1435
+ high: z.number()
1436
+ });
1437
+ /**
1438
+ * One `chat_template_kwargs` or `chat_template_args` value. The `$var` member
1439
+ * is pi-ai's placeholder for a value dispatch fills from the request's
1440
+ * thinking state, which makes a template-driven gateway configurable without
1441
+ * restating its template.
1442
+ */
1443
+ const chatTemplateKwarg = z.union([
1444
+ z.string(),
1445
+ z.number(),
1446
+ z.boolean(),
1447
+ z.const(null),
1448
+ z.object({
1449
+ $var: z.union(CHAT_TEMPLATE_VARS).required(),
1450
+ omitWhenOff: z.boolean()
1451
+ })
1452
+ ]);
1453
+ const compatProfile = z.object({
1454
+ supportsStore: z.boolean(),
1455
+ supportsDeveloperRole: z.boolean(),
1456
+ supportsReasoningEffort: z.boolean(),
1457
+ supportsUsageInStreaming: z.boolean(),
1458
+ supportsFinishReason: z.boolean(),
1459
+ maxTokensField: z.union(MAX_TOKENS_FIELDS),
1460
+ requiresToolResultName: z.boolean(),
1461
+ requiresAssistantAfterToolResult: z.boolean(),
1462
+ requiresThinkingAsText: z.boolean(),
1463
+ requiresReasoningContentOnAssistantMessages: z.boolean(),
1464
+ thinkingFormat: z.union(SUPPORTED_THINKING_FORMATS),
1465
+ chatTemplateKwargs: z.dict(chatTemplateKwarg),
1466
+ chatTemplateArgs: z.dict(chatTemplateKwarg),
1467
+ supportsThinkingTokenBudget: z.boolean(),
1468
+ supportsStrictMode: z.boolean(),
1469
+ cacheControlFormat: z.union(CACHE_CONTROL_FORMATS),
1470
+ supportsLongCacheRetention: z.boolean(),
1471
+ supportsEagerToolInputStreaming: z.boolean(),
1472
+ supportsCacheControlOnTools: z.boolean(),
1473
+ supportsTemperature: z.boolean(),
1474
+ forceAdaptiveThinking: z.boolean(),
1475
+ allowEmptySignature: z.boolean(),
1476
+ supportsStrictTools: z.boolean()
1477
+ });
1478
+ /**
1479
+ * Keys are the offered levels, values their wire spellings. A valueless key
1480
+ * (`off:`) survives validation because schemastery passes nullable data
1481
+ * through before any member schema runs — `z.const(null)` only controls the
1482
+ * error for non-null wrong values and what a configuration UI renders.
1483
+ * Only resolution decides which levels may leave the value empty, so the
1484
+ * diagnostic can name the route and model. The assertion narrows
1485
+ * schemastery's `Dict`, which types every literal key as required; dict
1486
+ * validation checks only present keys, so the runtime value is a partial record.
1487
+ */
1488
+ const reasoningEfforts = z.dict(z.union([z.string(), z.const(null)]), z.union(THINKING_LEVELS));
1489
+ /** The fields a `models` entry and a `modelOverrides` value share; only the id's home differs. */
1490
+ const modelFields = {
1491
+ name: z.string(),
1492
+ contextWindow: z.number().step(1).min(1),
1493
+ maxTokens: z.number().step(1).min(1),
1494
+ input: z.array(z.union(MODALITIES)),
1495
+ reasoningEfforts: z.union([z.const(false), reasoningEfforts]),
1496
+ compat: compatProfile
1497
+ };
1498
+ const modelProfile = z.object({
1499
+ id: z.string().required(),
1500
+ ...modelFields
1501
+ });
1502
+ /** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */
1503
+ const modelOverride = z.object(modelFields);
1504
+ const profile = z.object({
1505
+ apiKeyEnv: z.string().role("credential-ref"),
1506
+ displayName: z.string(),
1507
+ api: z.union(supportedProtocols()),
1508
+ baseURL: z.string(),
1509
+ models: z.array(modelProfile),
1510
+ modelOverrides: z.dict(modelOverride),
1511
+ compat: compatProfile,
1512
+ defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
1513
+ defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
1514
+ defaultInput: z.array(z.union(MODALITIES)).default([...DEFAULT_INPUT]),
1515
+ headers: z.dict(z.string()),
1516
+ reasoning: z.union(THINKING_LEVELS),
1517
+ reasoningSplit: z.boolean(),
1518
+ thinkingBudgets,
1519
+ cacheRetention: z.union([
1520
+ "none",
1521
+ "short",
1522
+ "long"
1523
+ ]),
1524
+ transport: z.union([
1525
+ "sse",
1526
+ "websocket",
1527
+ "websocket-cached",
1528
+ "auto"
1529
+ ]),
1530
+ timeoutMs: z.natural(),
1531
+ websocketConnectTimeoutMs: z.natural(),
1532
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
1533
+ maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
1534
+ requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
1535
+ requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
1536
+ retryPolicy: RetryPolicySchema
1537
+ });
1538
+ z.object({ providers: z.dict(profile).default({}) });
1539
+ /** Reject removed pre-release profile fields and name their replacements. */
1540
+ function rejectRemovedFields(provider, source) {
1541
+ const legacy = source;
1542
+ if ("provider" in legacy) throw new Error(`llm-pi-ai: provider "${provider}" sets "provider", which moved to the providers dict key`);
1543
+ if ("maxRetries" in legacy || "maxRetryDelayMs" in legacy) throw new Error(`llm-pi-ai: provider "${provider}" sets maxRetries or maxRetryDelayMs, which were removed; compose agent recovery with dsh-llm-retry`);
1544
+ }
1545
+ /**
1546
+ * Validate profiles and return a detached route-keyed map suitable for
1547
+ * per-request reads. This is the one explicit resolve step, so an omitted dict
1548
+ * resolves to the empty (dormant) route set here rather than through a hidden
1549
+ * fallback, and each route's models and pi-ai provider are materialized once.
1550
+ * @param providers - configured provider profiles keyed by route.
1551
+ * @returns validated profiles in configuration order.
1552
+ */
1553
+ function resolveProfiles(providers) {
1554
+ if (Array.isArray(providers)) throw new Error("llm-pi-ai: providers is now a dict keyed by provider route, not an array of profiles");
1555
+ const entries = Object.entries(providers ?? {});
1556
+ const resolved = /* @__PURE__ */ new Map();
1557
+ for (const [provider, source] of entries) {
1558
+ rejectRemovedFields(provider, source);
1559
+ if (provider.length === 0) throw new Error("llm-pi-ai: provider names must be non-empty");
1560
+ if (source.baseURL !== void 0 && source.baseURL.length === 0) throw new Error(`llm-pi-ai: provider "${provider}" has an empty baseURL`);
1561
+ if (source.displayName !== void 0 && source.displayName.length === 0) throw new Error(`llm-pi-ai: provider "${provider}" has an empty displayName`);
1562
+ const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? 3e5;
1563
+ if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`llm-pi-ai: provider "${provider}" streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
1564
+ const maxRequestImageBytes = source.maxRequestImageBytes ?? 20971520;
1565
+ if (!Number.isInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) throw new Error(`llm-pi-ai: provider "${provider}" maxRequestImageBytes must be a positive integer`);
1566
+ const requestImagePixelBudget = source.requestImagePixelBudget ?? 4194304;
1567
+ if (!Number.isSafeInteger(requestImagePixelBudget) || requestImagePixelBudget <= 0) throw new Error(`llm-pi-ai: provider "${provider}" requestImagePixelBudget must be a positive safe integer`);
1568
+ const requestImageMaxBytes = source.requestImageMaxBytes ?? 1048576;
1569
+ if (!Number.isSafeInteger(requestImageMaxBytes) || requestImageMaxBytes <= 0) throw new Error(`llm-pi-ai: provider "${provider}" requestImageMaxBytes must be a positive safe integer`);
1570
+ const defaultInput = [...source.defaultInput ?? DEFAULT_INPUT];
1571
+ if (defaultInput.length === 0) throw new Error(`llm-pi-ai: provider "${provider}" defaultInput must name at least one modality`);
1572
+ const displayName = source.displayName ?? provider;
1573
+ const catalog = resolveRouteModels({
1574
+ provider,
1575
+ ...source.api === void 0 ? {} : { api: source.api },
1576
+ ...source.baseURL === void 0 ? {} : { baseURL: source.baseURL },
1577
+ ...source.models === void 0 ? {} : { models: source.models },
1578
+ ...source.modelOverrides === void 0 ? {} : { modelOverrides: source.modelOverrides },
1579
+ ...source.compat === void 0 ? {} : { compat: source.compat },
1580
+ defaultInput,
1581
+ defaultContextWindow: source.defaultContextWindow ?? 262144,
1582
+ defaultMaxTokens: source.defaultMaxTokens ?? 32768
1583
+ });
1584
+ if (source.reasoningSplit !== void 0 && catalog.models.some((model) => model.api !== "openai-completions")) throw new Error(`llm-pi-ai: provider "${provider}" reasoningSplit requires every model to use openai-completions`);
1585
+ const { apiKeyEnv, retryPolicy, models: _models, displayName: _displayName, ...rest } = source;
1586
+ resolved.set(provider, {
1587
+ ...rest,
1588
+ provider,
1589
+ displayName,
1590
+ ...apiKeyEnv === void 0 ? {} : { apiKeyEnv: credentialRef(apiKeyEnv) },
1591
+ streamIdleTimeoutMs,
1592
+ maxRequestImageBytes,
1593
+ requestImagePixelBudget,
1594
+ requestImageMaxBytes,
1595
+ retryPolicy: resolveRetryPolicy(retryPolicy, `llm-pi-ai: provider "${provider}" retryPolicy`),
1596
+ ...rest.headers === void 0 ? {} : { headers: { ...rest.headers } },
1597
+ ...rest.thinkingBudgets === void 0 ? {} : { thinkingBudgets: { ...rest.thinkingBudgets } },
1598
+ configuredMaxTokens: catalog.configuredMaxTokens,
1599
+ piProvider: buildProvider({
1600
+ provider,
1601
+ displayName,
1602
+ ...source.api === void 0 ? {} : { api: source.api },
1603
+ ...source.baseURL === void 0 ? {} : { baseURL: source.baseURL },
1604
+ models: catalog.models,
1605
+ namesCredential: apiKeyEnv !== void 0
1606
+ })
1607
+ });
1608
+ }
1609
+ return resolved;
1610
+ }
1611
+ //#endregion
1612
+ //#region vendor/dsh-llm-pi-ai/src/context.ts
1613
+ /**
1614
+ * ChatCode CLI request-history conversion into pi-ai's Context vocabulary.
1615
+ *
1616
+ * @module dsh-llm-pi-ai/context
1617
+ */
1618
+ /** Join the text blocks of a harness message. */
1619
+ function flattenText(message) {
1620
+ return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1621
+ }
1622
+ /** Flatten text recursively inside one tool result. */
1623
+ function toolResultText(blocks) {
1624
+ return blocks.map((block) => block.type === "text" ? block.text : block.type === "tool-result" ? toolResultText(block.content) : "").join("");
1625
+ }
1626
+ /** Reject image roles that pi-ai cannot replay before request-size offloading can replace them. */
1627
+ function assertSupportedImageRoles(messages) {
1628
+ for (const message of messages) if (message.role !== "user" && contentHasImage(message.content)) throw new LlmError(`pi-ai cannot represent an image in an in-history ${message.role} message`, "UNSUPPORTED_CONTENT");
1629
+ }
1630
+ async function userContent(blocks, requestImages, resolveImageAccess) {
1631
+ const content = [];
1632
+ for (const block of blocks) switch (block.type) {
1633
+ case "text":
1634
+ if (block.text.length > 0) content.push({
1635
+ type: "text",
1636
+ text: block.text
1637
+ });
1638
+ break;
1639
+ case "image": {
1640
+ const version = requestImages.get(block.attachment.attachmentId);
1641
+ content.push({
1642
+ type: "text",
1643
+ text: requestImageHandleText(block.attachment, version, resolveImageAccess(block.attachment))
1644
+ });
1645
+ content.push({
1646
+ type: "image",
1647
+ data: Buffer.from(version.data).toString("base64"),
1648
+ mimeType: version.mediaType
1649
+ });
1650
+ break;
1651
+ }
1652
+ case "tool-result": {
1653
+ const nested = await userContent(block.content, requestImages, resolveImageAccess);
1654
+ if (typeof nested === "string") {
1655
+ if (nested.length > 0) content.push({
1656
+ type: "text",
1657
+ text: nested
1658
+ });
1659
+ } else content.push(...nested);
1660
+ }
1661
+ }
1662
+ if (content.every((block) => block.type === "text")) return content.map((block) => block.text).join("");
1663
+ return content;
1664
+ }
1665
+ function collectImageRefs(blocks, refs) {
1666
+ for (const block of blocks) if (block.type === "image") {
1667
+ if (block.offloaded !== true) refs.set(block.attachment.attachmentId, block.attachment);
1668
+ } else if (block.type === "tool-result") collectImageRefs(block.content, refs);
1669
+ }
1670
+ async function prepareRequestImages(messages, attachments, budget, signal) {
1671
+ const refs = /* @__PURE__ */ new Map();
1672
+ for (const message of messages) collectImageRefs(message.content, refs);
1673
+ const orderedRefs = [...refs.values()];
1674
+ const prepared = await Promise.all(orderedRefs.map((ref) => attachments.readImageRequest(ref, requestImageTarget(ref, budget), signal)));
1675
+ const versions = /* @__PURE__ */ new Map();
1676
+ for (const [index, ref] of orderedRefs.entries()) versions.set(ref.attachmentId, prepared[index]);
1677
+ return versions;
1678
+ }
1679
+ function toolsOf(options) {
1680
+ return options.tools?.map((tool) => ({
1681
+ name: tool.name,
1682
+ description: tool.description,
1683
+ parameters: tool.parameters
1684
+ }));
1685
+ }
1686
+ /** Select the pi-ai system prompt source shared by both conversion paths. */
1687
+ function splitSystemPrompt(options) {
1688
+ if (options.system !== void 0) return {
1689
+ systemPrompt: options.system,
1690
+ messages: options.messages
1691
+ };
1692
+ const [first, ...rest] = options.messages;
1693
+ if (first?.role !== "system") return {
1694
+ systemPrompt: void 0,
1695
+ messages: options.messages
1696
+ };
1697
+ const text = flattenText(first);
1698
+ return {
1699
+ systemPrompt: text.length > 0 ? text : void 0,
1700
+ messages: rest
1701
+ };
1702
+ }
1703
+ /** Assemble the request-level pi-ai context envelope shared by both conversion paths. */
1704
+ function piContext(systemPrompt, options, messages) {
1705
+ const tools = toolsOf(options);
1706
+ return {
1707
+ ...systemPrompt !== void 0 ? { systemPrompt } : {},
1708
+ messages,
1709
+ ...tools !== void 0 && tools.length > 0 ? { tools } : {}
1710
+ };
1711
+ }
1712
+ function appendAssistant(message, messages, toolNames, onReplayDegrade) {
1713
+ const assistant = toPiAssistant(message, onReplayDegrade);
1714
+ for (const block of assistant.content) if (block.type === "toolCall") toolNames.set(brandString(block.id), block.name);
1715
+ messages.push(assistant);
1716
+ }
1717
+ function textOnlyContext(options, onReplayDegrade) {
1718
+ assertSupportedImageRoles(options.messages);
1719
+ const split = splitSystemPrompt(options);
1720
+ const toolNames = /* @__PURE__ */ new Map();
1721
+ const messages = [];
1722
+ for (const message of split.messages) {
1723
+ if (contentHasImage(message.content)) throw new LlmError("pi-ai image conversion requires the durable attachment service", "UNSUPPORTED_CONTENT");
1724
+ if (message.role === "system") {
1725
+ messages.push({
1726
+ role: "user",
1727
+ content: flattenText(message),
1728
+ timestamp: 0
1729
+ });
1730
+ continue;
1731
+ }
1732
+ if (message.role === "assistant") {
1733
+ appendAssistant(message, messages, toolNames, onReplayDegrade);
1734
+ continue;
1735
+ }
1736
+ const text = flattenText(message);
1737
+ const results = message.content.filter((block) => block.type === "tool-result");
1738
+ if (text.length > 0 || results.length === 0) messages.push({
1739
+ role: "user",
1740
+ content: text,
1741
+ timestamp: 0
1742
+ });
1743
+ for (const result of results) messages.push({
1744
+ role: "toolResult",
1745
+ toolCallId: result.toolCallId,
1746
+ toolName: toolNames.get(result.toolCallId) ?? "unknown",
1747
+ content: [{
1748
+ type: "text",
1749
+ text: toolResultText(result.content) || "(no output)"
1750
+ }],
1751
+ isError: result.isError ?? false,
1752
+ timestamp: 0
1753
+ });
1754
+ }
1755
+ return piContext(split.systemPrompt, options, messages);
1756
+ }
1757
+ /** Deterministic request target for one source under the route budgets. */
1758
+ function requestImageTarget(ref, budget) {
1759
+ return {
1760
+ ...requestImageDimensions(ref.width, ref.height, budget.maxPixels),
1761
+ maxBytes: budget.maxBytes
1762
+ };
1763
+ }
1764
+ function toPiContext(options, images, onReplayDegrade) {
1765
+ return images === void 0 ? textOnlyContext(options, onReplayDegrade) : toPiContextWithImages(options, images, onReplayDegrade);
1766
+ }
1767
+ async function toPiContextWithImages(options, images, onReplayDegrade) {
1768
+ const { attachments, resolveImageAccess, maxRequestImageBytes } = images;
1769
+ const requestImagePolicy = images.requestImagePolicy ?? {
1770
+ maxPixels: 4194304,
1771
+ maxBytes: 1048576
1772
+ };
1773
+ assertSupportedImageRoles(options.messages);
1774
+ const split = splitSystemPrompt(options);
1775
+ const requestImages = await prepareRequestImages(split.messages, attachments, requestImagePolicy, options.signal);
1776
+ if (maxRequestImageBytes !== void 0) {
1777
+ const offloadImages = requiredImageOffload(split.messages, {
1778
+ representation: "base64",
1779
+ maxBytes: maxRequestImageBytes
1780
+ }, (block) => requestImages.get(block.attachment.attachmentId).bytes);
1781
+ if (offloadImages > 0) throw new LlmError(`pi-ai request images exceed the ${maxRequestImageBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
1782
+ }
1783
+ const exactMessages = projectOffloadedImages(split.messages, (ref) => offloadedImageText(ref, resolveImageAccess(ref)));
1784
+ const toolNames = /* @__PURE__ */ new Map();
1785
+ const messages = [];
1786
+ for (const message of exactMessages) {
1787
+ if (message.role === "system") {
1788
+ messages.push({
1789
+ role: "user",
1790
+ content: flattenText(message),
1791
+ timestamp: 0
1792
+ });
1793
+ continue;
1794
+ }
1795
+ if (message.role === "assistant") {
1796
+ appendAssistant(message, messages, toolNames, onReplayDegrade);
1797
+ continue;
1798
+ }
1799
+ const content = await userContent(message.content.filter((block) => block.type !== "tool-result"), requestImages, resolveImageAccess);
1800
+ const results = message.content.filter((block) => block.type === "tool-result");
1801
+ if (content.length > 0 || results.length === 0) messages.push({
1802
+ role: "user",
1803
+ content,
1804
+ timestamp: 0
1805
+ });
1806
+ for (const result of results) {
1807
+ const resultContent = await userContent(result.content, requestImages, resolveImageAccess);
1808
+ messages.push({
1809
+ role: "toolResult",
1810
+ toolCallId: result.toolCallId,
1811
+ toolName: toolNames.get(result.toolCallId) ?? "unknown",
1812
+ content: typeof resultContent === "string" ? [{
1813
+ type: "text",
1814
+ text: resultContent || "(no output)"
1815
+ }] : resultContent,
1816
+ isError: result.isError ?? false,
1817
+ timestamp: 0
1818
+ });
1819
+ }
1820
+ }
1821
+ return piContext(split.systemPrompt, options, messages);
1822
+ }
1823
+ //#endregion
1824
+ //#region vendor/dsh-llm-pi-ai/src/stream.ts
1825
+ /**
1826
+ * pi-ai assistant event translation into the ChatCode CLI streaming protocol.
1827
+ *
1828
+ * pi-ai tool-call arguments are parsed objects while ChatCode CLI keeps their
1829
+ * raw JSON representation. pi-ai also reports failures as terminal stream
1830
+ * events, which this module maps into ChatCode CLI finish chunks.
1831
+ *
1832
+ * @module dsh-llm-pi-ai/stream
1833
+ */
1834
+ /**
1835
+ * Map pi-ai usage (reasoning folded into output by pi-ai).
1836
+ * @param usage - cumulative usage from the terminal pi-ai event.
1837
+ * @returns harness counts with pi-ai's exact total; cache fields appear only
1838
+ * when non-zero (pi-ai reports zeros, not absence).
1839
+ */
1840
+ function mapUsage(usage) {
1841
+ return {
1842
+ inputTokens: usage.input,
1843
+ outputTokens: usage.output,
1844
+ totalTokens: usage.totalTokens,
1845
+ ...usage.cacheRead > 0 ? { cacheReadTokens: usage.cacheRead } : {},
1846
+ ...usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {}
1847
+ };
1848
+ }
1849
+ function classifyPiAiError(message) {
1850
+ if (/\b(?:401|403)\b/.test(message)) return "AUTH";
1851
+ if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
1852
+ if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
1853
+ if (/\b413\b|failed to buffer the request body:\s*length limit exceeded|payload too large|request body too large/i.test(message)) return "INVALID_REQUEST";
1854
+ if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
1855
+ if (/\b5\d\d\b/.test(message)) return "SERVER";
1856
+ if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return "TIMEOUT";
1857
+ if (/stream ended (?:before|without)\b/i.test(message)) return "TRANSPORT";
1858
+ if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(message) || /\b(?:other side closed|HTTP2 request did not get a response|WebSocket closed unexpectedly)\b/i.test(message) || /\bterminated\b|premature close/i.test(message)) return "TRANSPORT";
1859
+ return "PI_AI_ERROR";
1860
+ }
1861
+ /**
1862
+ * Map a terminal pi-ai event to the harness finish reason.
1863
+ * @param message - the assistant message carried by the `done` or `error` event.
1864
+ * @param contextWindow - resolved catalog capacity for usage-based overflow detection.
1865
+ * @returns the mapped harness reason. Recognized error text, `stop` usage above
1866
+ * `contextWindow`, and zero-output `length` usage that fills the window map
1867
+ * to `CONTEXT_WINDOW_EXCEEDED`; a `stop` with no content blocks maps to an
1868
+ * `EMPTY_RESPONSE` error, while terminal `pending` and `deferred` states map
1869
+ * to non-retryable `PI_AI_ERROR` failures.
1870
+ */
1871
+ function mapStopReason(message, contextWindow) {
1872
+ const piAiOverflow = isContextOverflow(message, contextWindow);
1873
+ const harnessOverflow = message.stopReason === "error" && message.errorMessage !== void 0 && isContextWindowExceededError(message.errorMessage);
1874
+ if (piAiOverflow || harnessOverflow) return {
1875
+ kind: "error",
1876
+ failure: {
1877
+ message: message.errorMessage ?? `pi-ai detected context overflow for model "${message.model}"`,
1878
+ code: CONTEXT_WINDOW_EXCEEDED_CODE
1879
+ }
1880
+ };
1881
+ switch (message.stopReason) {
1882
+ case "stop":
1883
+ if (message.content.length === 0) return {
1884
+ kind: "error",
1885
+ failure: {
1886
+ message: `model "${message.model}" returned a completed response with no content`,
1887
+ code: EMPTY_RESPONSE_CODE
1888
+ }
1889
+ };
1890
+ return { kind: "stop" };
1891
+ case "length": return { kind: "max-tokens" };
1892
+ case "toolUse": return { kind: "tool-calls" };
1893
+ case "pending": return {
1894
+ kind: "error",
1895
+ failure: {
1896
+ message: `pi-ai stream for model "${message.model}" ended pending`,
1897
+ code: "PI_AI_ERROR"
1898
+ }
1899
+ };
1900
+ case "deferred": return {
1901
+ kind: "error",
1902
+ failure: {
1903
+ message: `pi-ai deferred response for model "${message.model}" is not supported`,
1904
+ code: "PI_AI_ERROR"
1905
+ }
1906
+ };
1907
+ case "aborted": return {
1908
+ kind: "aborted",
1909
+ failure: {
1910
+ message: message.errorMessage ?? "pi-ai stream aborted",
1911
+ code: "ABORTED"
1912
+ }
1913
+ };
1914
+ case "error": {
1915
+ const text = message.errorMessage ?? "pi-ai stream error";
1916
+ return {
1917
+ kind: "error",
1918
+ failure: {
1919
+ message: text,
1920
+ code: classifyPiAiError(text)
1921
+ }
1922
+ };
1923
+ }
1924
+ }
1925
+ }
1926
+ /**
1927
+ * Translate the pi-ai event stream into StreamChunks. pi-ai never throws
1928
+ * mid-stream — failures arrive as `error` events, which become error/aborted
1929
+ * `finish` chunks (the harness protocol's other error-delivery style).
1930
+ * @param events - one assistant turn's pi-ai event stream.
1931
+ * @param contextWindow - resolved catalog capacity for usage-based overflow detection.
1932
+ * @param callerSignal - caller cancellation state; an aborted caller makes any
1933
+ * in-band terminal error an aborted finish.
1934
+ * @returns the harness chunks, ending with `usage` then `finish`; throws
1935
+ * `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
1936
+ */
1937
+ async function* toStreamChunks(events, contextWindow, callerSignal) {
1938
+ const toolIds = /* @__PURE__ */ new Map();
1939
+ for await (const event of events) switch (event.type) {
1940
+ case "start": break;
1941
+ case "text_start":
1942
+ yield {
1943
+ type: "block-start",
1944
+ index: event.contentIndex,
1945
+ blockType: "text"
1946
+ };
1947
+ break;
1948
+ case "text_delta":
1949
+ yield {
1950
+ type: "text-delta",
1951
+ index: event.contentIndex,
1952
+ text: event.delta
1953
+ };
1954
+ break;
1955
+ case "text_end":
1956
+ yield {
1957
+ type: "block-end",
1958
+ index: event.contentIndex,
1959
+ block: {
1960
+ type: "text",
1961
+ text: event.content
1962
+ }
1963
+ };
1964
+ break;
1965
+ case "thinking_start":
1966
+ yield {
1967
+ type: "block-start",
1968
+ index: event.contentIndex,
1969
+ blockType: "reasoning"
1970
+ };
1971
+ break;
1972
+ case "thinking_delta":
1973
+ yield {
1974
+ type: "reasoning-delta",
1975
+ index: event.contentIndex,
1976
+ text: event.delta
1977
+ };
1978
+ break;
1979
+ case "thinking_end":
1980
+ yield {
1981
+ type: "block-end",
1982
+ index: event.contentIndex,
1983
+ block: {
1984
+ type: "reasoning",
1985
+ text: event.content
1986
+ }
1987
+ };
1988
+ break;
1989
+ case "toolcall_start": {
1990
+ const partial = event.partial.content[event.contentIndex];
1991
+ const id = partial?.type === "toolCall" ? partial.id : "";
1992
+ const name = partial?.type === "toolCall" ? partial.name : "";
1993
+ toolIds.set(event.contentIndex, {
1994
+ id,
1995
+ name
1996
+ });
1997
+ yield {
1998
+ type: "block-start",
1999
+ index: event.contentIndex,
2000
+ blockType: "tool-call"
2001
+ };
2002
+ break;
2003
+ }
2004
+ case "toolcall_delta": {
2005
+ const known = toolIds.get(event.contentIndex);
2006
+ yield {
2007
+ type: "tool-call-delta",
2008
+ index: event.contentIndex,
2009
+ id: brandString(known?.id ?? ""),
2010
+ ...known?.name !== void 0 && known.name.length > 0 ? { name: known.name } : {},
2011
+ argumentsDelta: event.delta
2012
+ };
2013
+ break;
2014
+ }
2015
+ case "toolcall_end":
2016
+ yield {
2017
+ type: "block-end",
2018
+ index: event.contentIndex,
2019
+ block: {
2020
+ type: "tool-call",
2021
+ id: brandString(event.toolCall.id),
2022
+ name: event.toolCall.name,
2023
+ arguments: JSON.stringify(event.toolCall.arguments)
2024
+ }
2025
+ };
2026
+ break;
2027
+ case "done":
2028
+ yield {
2029
+ type: "usage",
2030
+ usage: mapUsage(event.message.usage)
2031
+ };
2032
+ yield {
2033
+ type: "finish",
2034
+ reason: mapStopReason(event.message, contextWindow),
2035
+ replayState: toPiReplayState(event.message)
2036
+ };
2037
+ return;
2038
+ case "error":
2039
+ yield {
2040
+ type: "usage",
2041
+ usage: mapUsage(event.error.usage)
2042
+ };
2043
+ yield {
2044
+ type: "finish",
2045
+ reason: mapStopReason(callerSignal?.aborted ? {
2046
+ ...event.error,
2047
+ stopReason: "aborted"
2048
+ } : event.error, contextWindow)
2049
+ };
2050
+ return;
2051
+ }
2052
+ throw new LlmError("pi-ai event stream ended without done/error", "STREAM_CLOSED");
2053
+ }
2054
+ //#endregion
2055
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/usingCtx.js
2056
+ function _usingCtx() {
2057
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
2058
+ var n = Error();
2059
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
2060
+ }, e = {}, n = [];
2061
+ function using(r, e) {
2062
+ if (null != e) {
2063
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
2064
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
2065
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
2066
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
2067
+ t && (o = function o() {
2068
+ try {
2069
+ t.call(e);
2070
+ } catch (r) {
2071
+ return Promise.reject(r);
2072
+ }
2073
+ }), n.push({
2074
+ v: e,
2075
+ d: o,
2076
+ a: r
2077
+ });
2078
+ } else r && n.push({
2079
+ d: e,
2080
+ a: r
2081
+ });
2082
+ return e;
2083
+ }
2084
+ return {
2085
+ e,
2086
+ u: using.bind(null, !1),
2087
+ a: using.bind(null, !0),
2088
+ d: function d() {
2089
+ var o, t = this.e, s = 0;
2090
+ function next() {
2091
+ for (; o = n.pop();) try {
2092
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
2093
+ if (o.d) {
2094
+ var r = o.d.call(o.v);
2095
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
2096
+ } else s |= 1;
2097
+ } catch (r) {
2098
+ return err(r);
2099
+ }
2100
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
2101
+ if (t !== e) throw t;
2102
+ }
2103
+ function err(n) {
2104
+ return t = t !== e ? new r(n, t) : n, next();
2105
+ }
2106
+ return next();
2107
+ }
2108
+ };
2109
+ }
2110
+ //#endregion
2111
+ //#region vendor/dsh-llm-pi-ai/src/adapter.ts
2112
+ /**
2113
+ * Generic pi-ai-backed implementation of the ChatCode CLI LLM seam.
2114
+ *
2115
+ * Each resolution produces one **immutable** snapshot — the profiles plus a
2116
+ * `Models` collection holding the `Provider` each route built — and an
2117
+ * operation captures a whole snapshot before its first `await`. A
2118
+ * configuration change builds a *new* collection rather than mutating the one
2119
+ * in use, because `Models.streamSimple()` is lazy: it resolves the provider
2120
+ * when the stream is first consumed, which is after the credential await, so a
2121
+ * mutated collection would let a request that started under one configuration
2122
+ * finish under another — or fail with a provider that no longer exists. This is
2123
+ * what makes the seam's per-step call freeze (`llm.prepareCall()`) hold all the
2124
+ * way down: switching models mid-reply takes effect on the next step, never
2125
+ * inside the one in flight.
2126
+ *
2127
+ * A route naming a credential reference still resolves it through the harness
2128
+ * seam and passes it as the request's `apiKey` option, which pi-ai treats as
2129
+ * the highest-priority auth override — that is what keeps the fail-loud
2130
+ * reference semantics. Everything that override does not cover reaches pi-ai
2131
+ * through the collection's own auth: the credential store holds the records a
2132
+ * login wrote and a refresh rotates, and the auth context answers the ambient
2133
+ * questions a provider asks while resolving. Both are stable across snapshots,
2134
+ * so a configuration change rebuilds the collection without forgetting who is
2135
+ * signed in.
2136
+ *
2137
+ * @module dsh-llm-pi-ai/adapter
2138
+ */
2139
+ /** Copy profile stream knobs into pi-ai's common option vocabulary. */
2140
+ function profileOptions(profile, reasoning, apiKey) {
2141
+ const enabledReasoning = reasoning === "off" ? void 0 : reasoning;
2142
+ return {
2143
+ ...apiKey === void 0 ? {} : { apiKey },
2144
+ ...enabledReasoning === void 0 ? {} : { reasoning: enabledReasoning },
2145
+ ...profile.reasoningSplit === void 0 ? {} : { samplingParams: { reasoning_split: profile.reasoningSplit } },
2146
+ ...profile.thinkingBudgets === void 0 ? {} : { thinkingBudgets: profile.thinkingBudgets },
2147
+ ...profile.cacheRetention === void 0 ? {} : { cacheRetention: profile.cacheRetention },
2148
+ ...profile.transport === void 0 ? {} : { transport: profile.transport },
2149
+ ...profile.timeoutMs === void 0 ? {} : { timeoutMs: profile.timeoutMs },
2150
+ ...profile.websocketConnectTimeoutMs === void 0 ? {} : { websocketConnectTimeoutMs: profile.websocketConnectTimeoutMs },
2151
+ maxRetries: 0
2152
+ };
2153
+ }
2154
+ /**
2155
+ * The profile default this exact model can actually take, for DESCRIBING it.
2156
+ * A configured level the model does not support yields none rather than
2157
+ * throwing: `resolveModel` builds the model catalog, and a catalog that fails
2158
+ * takes its whole provider out of every picker — so one mis-set profile field
2159
+ * would hide every model on the route, including the ones that support the
2160
+ * level. The request path still refuses, which is where a bad configuration
2161
+ * belongs: describing what a model can do must not fail because a deployment
2162
+ * asked it for something it cannot.
2163
+ * @param model - the resolved model descriptor.
2164
+ * @param effort - the profile's configured level, if any.
2165
+ * @returns the level when this model supports it, otherwise undefined.
2166
+ */
2167
+ function describableReasoningLevel(model, effort) {
2168
+ if (effort === void 0) return void 0;
2169
+ return getSupportedThinkingLevels(model).some((level) => level === effort) ? effort : void 0;
2170
+ }
2171
+ /** Validate an explicit ChatCode CLI profile effort without invoking pi-ai's clamp. */
2172
+ function resolveReasoningLevel(model, effort) {
2173
+ if (effort === void 0) return void 0;
2174
+ if (getSupportedThinkingLevels(model).some((level) => level === effort)) return effort;
2175
+ throw new LlmError(`pi-ai provider "${model.provider}" model "${model.id}" does not support reasoning effort "${effort}"`, "UNSUPPORTED_REASONING_EFFORT");
2176
+ }
2177
+ /**
2178
+ * Selectable reasoning efforts for one model, or nothing at all.
2179
+ *
2180
+ * A model that carries no reasoning metadata — every hand-declared one, and
2181
+ * every catalog model pi-ai marks as non-reasoning — is reported by pi-ai as
2182
+ * supporting the single level `off`. Passing that through would offer a control
2183
+ * that cannot do what it says: `off` is translated to *omitting* the reasoning
2184
+ * option, which for such a model is byte-for-byte the same request as naming no
2185
+ * effort — so a provider whose own default is to think would keep thinking with
2186
+ * `off` selected. Omitting `reasoning` entirely is the seam's way of saying the
2187
+ * capability is unavailable, which leaves the surface offering only the
2188
+ * provider's default.
2189
+ * @param model - the resolved model descriptor.
2190
+ * @param defaultLevel - the profile's configured effort, already validated.
2191
+ * @returns the `reasoning` field, or an empty object when none can be offered.
2192
+ */
2193
+ function reasoningInfo(model, defaultLevel) {
2194
+ if (!model.reasoning) return {};
2195
+ return { reasoning: {
2196
+ efforts: getSupportedThinkingLevels(model).map((level) => ({
2197
+ id: ReasoningEffortId(level),
2198
+ name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
2199
+ })),
2200
+ ...defaultLevel === void 0 ? {} : { defaultEffort: ReasoningEffortId(defaultLevel) }
2201
+ } };
2202
+ }
2203
+ /** Merge deployment headers while removing case-insensitive attribution collisions. */
2204
+ function requestHeaders(headers, auth) {
2205
+ const attribution = attributionHeaders();
2206
+ const reserved = new Set([...Object.keys(attribution), ...Object.keys(auth ?? {})].map((name) => name.toLowerCase()));
2207
+ return {
2208
+ ...Object.fromEntries(Object.entries(headers ?? {}).filter(([name]) => !reserved.has(name.toLowerCase()))),
2209
+ ...Object.fromEntries(Object.entries(auth ?? {}).filter(([name]) => !Object.keys(attribution).some((reservedName) => reservedName.toLowerCase() === name.toLowerCase()))),
2210
+ ...attribution
2211
+ };
2212
+ }
2213
+ /**
2214
+ * pi-ai-backed multi-provider adapter. Each operation reads the current
2215
+ * profiles, so a configuration change reaches the next request without a
2216
+ * restart; model descriptors come from the collection those profiles built.
2217
+ */
2218
+ var PiAiAdapter = class extends LlmAdapter {
2219
+ config;
2220
+ snapshot;
2221
+ constructor(config) {
2222
+ super();
2223
+ this.config = config;
2224
+ }
2225
+ /**
2226
+ * The snapshot for the current profiles. Resolution memoizes its result, so
2227
+ * an unchanged configuration is recognized by identity; a changed one gets a
2228
+ * brand-new collection, leaving any snapshot an operation already captured
2229
+ * untouched for as long as that operation holds it.
2230
+ */
2231
+ current() {
2232
+ const profiles = this.config.profiles();
2233
+ if (this.snapshot?.profiles === profiles) return this.snapshot;
2234
+ const models = createModels(this.config.auth);
2235
+ for (const profile of profiles.values()) models.setProvider(profile.piProvider);
2236
+ this.snapshot = {
2237
+ profiles,
2238
+ models
2239
+ };
2240
+ return this.snapshot;
2241
+ }
2242
+ /** The profile for one route within one snapshot, or the not-owned failure. */
2243
+ profileOf(snapshot, provider) {
2244
+ const profile = snapshot.profiles.get(provider);
2245
+ if (profile === void 0) throw new LlmError(`pi-ai adapter does not own provider "${provider}"`, "NO_ADAPTER");
2246
+ return profile;
2247
+ }
2248
+ /** The configured descriptor for one exact route/model pair within one snapshot. */
2249
+ modelOf(snapshot, provider, model) {
2250
+ this.profileOf(snapshot, provider);
2251
+ const resolved = snapshot.models.getModel(provider, model);
2252
+ if (resolved === void 0) throw new LlmError(`pi-ai provider "${provider}" has no configured model "${model}"`, "UNKNOWN_MODEL");
2253
+ return resolved;
2254
+ }
2255
+ providerInfo(provider) {
2256
+ return {
2257
+ id: provider,
2258
+ name: this.current().profiles.get(provider)?.displayName ?? provider
2259
+ };
2260
+ }
2261
+ providerRetryPolicy(provider) {
2262
+ return this.current().profiles.get(provider)?.retryPolicy;
2263
+ }
2264
+ listModels(provider) {
2265
+ return Promise.resolve().then(() => {
2266
+ const snapshot = this.current();
2267
+ this.profileOf(snapshot, provider);
2268
+ return snapshot.models.getModels(provider).map((model) => ({
2269
+ provider,
2270
+ id: model.id,
2271
+ name: model.name,
2272
+ inputModalities: [...model.input]
2273
+ }));
2274
+ });
2275
+ }
2276
+ resolveModel(provider, model, _signal) {
2277
+ return Promise.resolve().then(() => {
2278
+ const snapshot = this.current();
2279
+ return this.modelInfo(snapshot, provider, model);
2280
+ });
2281
+ }
2282
+ modelInfo(snapshot, provider, model) {
2283
+ const profile = this.profileOf(snapshot, provider);
2284
+ const resolvedModel = this.modelOf(snapshot, provider, model);
2285
+ const defaultLevel = describableReasoningLevel(resolvedModel, profile.reasoning);
2286
+ const configuredMaxTokens = profile.configuredMaxTokens.get(model);
2287
+ return {
2288
+ provider,
2289
+ id: model,
2290
+ name: resolvedModel.name,
2291
+ inputModalities: [...resolvedModel.input],
2292
+ context: { contextWindow: resolvedModel.contextWindow },
2293
+ ...configuredMaxTokens === void 0 ? {} : { defaultMaxTokens: configuredMaxTokens },
2294
+ ...reasoningInfo(resolvedModel, defaultLevel)
2295
+ };
2296
+ }
2297
+ prepareCall(provider, model, _signal) {
2298
+ const snapshot = this.current();
2299
+ return Promise.resolve({
2300
+ model: this.modelInfo(snapshot, provider, model),
2301
+ stream: (options) => this.streamWithSnapshot(options, snapshot)
2302
+ });
2303
+ }
2304
+ stream(options) {
2305
+ return this.streamWithSnapshot(options, this.current());
2306
+ }
2307
+ async *streamWithSnapshot(options, snapshot) {
2308
+ try {
2309
+ var _usingCtx$1 = _usingCtx();
2310
+ if (options.stop !== void 0) throw new LlmError("llm-pi-ai does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
2311
+ const profile = this.profileOf(snapshot, options.provider);
2312
+ const model = this.modelOf(snapshot, options.provider, options.model);
2313
+ const reasoning = resolveReasoningLevel(model, options.reasoningEffort ?? profile.reasoning);
2314
+ const auth = await this.config.resolveAuth(options.provider, profile);
2315
+ const consumer = new AbortController();
2316
+ const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
2317
+ const streamIdleTimeoutMs = profile.streamIdleTimeoutMs;
2318
+ const watchdog = _usingCtx$1.u(idleWatchdog(upstream, streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
2319
+ try {
2320
+ const containsImage = options.messages.some((message) => contentHasImage(message.content));
2321
+ if (containsImage && !model.input.includes("image")) throw new LlmError(`pi-ai model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
2322
+ const attachments = containsImage ? this.config.resolveAttachments?.() : void 0;
2323
+ if (containsImage && attachments === void 0) throw new LlmError("pi-ai image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
2324
+ const onReplayDegrade = (reason) => {
2325
+ this.config.onReplayDegrade?.({
2326
+ provider: options.provider,
2327
+ model: options.model,
2328
+ reason
2329
+ });
2330
+ };
2331
+ const context = attachments === void 0 ? toPiContext(options, void 0, onReplayDegrade) : await toPiContext({
2332
+ ...options,
2333
+ signal: watchdog.signal
2334
+ }, {
2335
+ attachments,
2336
+ resolveImageAccess: (ref) => this.config.resolveImageAccess?.(attachments, ref),
2337
+ maxRequestImageBytes: profile.maxRequestImageBytes,
2338
+ requestImagePolicy: {
2339
+ maxPixels: profile.requestImagePixelBudget,
2340
+ maxBytes: profile.requestImageMaxBytes
2341
+ }
2342
+ }, onReplayDegrade);
2343
+ const iterator = toStreamChunks(snapshot.models.streamSimple(model, context, {
2344
+ ...profileOptions(profile, reasoning, auth.apiKey),
2345
+ ...options.temperature === void 0 ? {} : { temperature: options.temperature },
2346
+ ...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
2347
+ ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
2348
+ signal: watchdog.signal,
2349
+ headers: requestHeaders(profile.headers, auth.headers)
2350
+ }), model.contextWindow, options.signal)[Symbol.asyncIterator]();
2351
+ let exhausted = false;
2352
+ try {
2353
+ while (true) {
2354
+ const result = await watchdog.next(iterator);
2355
+ const timeout = timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT");
2356
+ if (timeout !== void 0) throw timeout;
2357
+ if (result.done) {
2358
+ exhausted = true;
2359
+ return;
2360
+ }
2361
+ yield result.value;
2362
+ }
2363
+ } finally {
2364
+ if (!exhausted) {
2365
+ consumer.abort("pi-ai stream consumer stopped");
2366
+ try {
2367
+ await iterator.return(void 0);
2368
+ } catch (_abortedSdkTeardown) {}
2369
+ }
2370
+ }
2371
+ } catch (error) {
2372
+ if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) throw new LlmError(`pi-ai stream idle timeout after ${streamIdleTimeoutMs}ms`, "TIMEOUT", { cause: error });
2373
+ if (options.signal?.aborted) throw new LlmError("pi-ai request aborted by caller", "ABORTED", { cause: error });
2374
+ throw error;
2375
+ } finally {
2376
+ consumer.abort("pi-ai stream consumer stopped");
2377
+ }
2378
+ } catch (_) {
2379
+ _usingCtx$1.e = _;
2380
+ } finally {
2381
+ _usingCtx$1.d();
2382
+ }
2383
+ }
2384
+ };
2385
+ //#endregion
2386
+ //#region src/adapter.ts
2387
+ /** Aggregate imported models while dispatching each private route through its selected adapter. @module dsh-llm-chatcode-config/adapter */
2388
+ /** One public provider route containing every imported ChatCode custom model. */
2389
+ const CHATCODE_PROVIDER = "chatcode-custom";
2390
+ /** Display name for the unified imported-model group. */
2391
+ const CHATCODE_PROVIDER_NAME = "自定义模型";
2392
+ /** Provider route and selector group for centrally managed Coding Plan models. */
2393
+ const CODING_PLAN_PROVIDER = "chatcode-codingplan";
2394
+ const CODING_PLAN_PROVIDER_NAME = "内置模型";
2395
+ /** Provider route and selector group for opt-in MAAS models. */
2396
+ const MAAS_PROVIDER = "chatcode-maas";
2397
+ const MAAS_PROVIDER_NAME = "Maas平台";
2398
+ const DEEPSEEK_MODEL = /deepseek/i;
2399
+ function privateModelKey(route, model) {
2400
+ return `${route}\u0000${model}`;
2401
+ }
2402
+ function createDeepSeekAdapter(route, profile, model, apiKey) {
2403
+ const protocol = profile.api === "anthropic-messages" ? "messages" : profile.api === "openai-completions" ? "chat-completions" : void 0;
2404
+ if (protocol === void 0 || profile.baseURL === void 0) throw new LlmError(`chatcode-config: DeepSeek model route "${route}" has no supported protocol or base URL`, "INVALID_CHATCODE_CONFIG");
2405
+ const connection = {
2406
+ ...resolveAdapterOptions({
2407
+ protocol,
2408
+ baseURL: profile.baseURL,
2409
+ defaultContextWindow: model.contextWindow,
2410
+ maxTokens: model.maxTokens,
2411
+ models: [{
2412
+ id: model.id,
2413
+ name: model.name,
2414
+ contextWindow: model.contextWindow,
2415
+ maxTokens: model.maxTokens,
2416
+ inputModalities: [...model.input]
2417
+ }],
2418
+ streamIdleTimeoutMs: profile.streamIdleTimeoutMs
2419
+ }),
2420
+ retryPolicy: profile.retryPolicy
2421
+ };
2422
+ return new DeepSeekAdapter({
2423
+ options: () => connection,
2424
+ resolveApiKey: () => {
2425
+ if (apiKey === void 0) throw new LlmError(`chatcode-config: no API key for DeepSeek model "${model.id}"`, "MISSING_CREDENTIAL");
2426
+ return Promise.resolve(apiKey);
2427
+ },
2428
+ resolveUserId: () => getOrCreateAnonymousUserId(),
2429
+ prepareExtensions: () => Promise.resolve({
2430
+ fields: {},
2431
+ accept: () => Promise.resolve()
2432
+ })
2433
+ });
2434
+ }
2435
+ /** A static unified ChatCode catalog with request adapters selected from actual model ids. */
2436
+ var ChatCodeAdapter = class extends LlmAdapter {
2437
+ profiles;
2438
+ provider;
2439
+ providerName;
2440
+ piAdapter;
2441
+ deepSeekAdapters = /* @__PURE__ */ new Map();
2442
+ routes = /* @__PURE__ */ new Map();
2443
+ publicModels = /* @__PURE__ */ new Map();
2444
+ constructor(options, profiles, provider = CHATCODE_PROVIDER, providerName = CHATCODE_PROVIDER_NAME, selections, apiKeys = /* @__PURE__ */ new Map()) {
2445
+ super();
2446
+ this.profiles = profiles;
2447
+ this.provider = provider;
2448
+ this.providerName = providerName;
2449
+ this.piAdapter = new PiAiAdapter(options);
2450
+ for (const [route, profile] of profiles) for (const model of profile.piProvider.getModels()) {
2451
+ const selection = [...selections ?? /* @__PURE__ */ new Map()].find(([, target]) => target.route === route && target.model === model.id)?.[0] ?? model.id;
2452
+ if (this.routes.has(selection)) throw new LlmError(`chatcode-config provider has duplicate public model "${selection}"`, "INVALID_CHATCODE_CONFIG");
2453
+ this.routes.set(selection, {
2454
+ selection,
2455
+ route,
2456
+ model: model.id
2457
+ });
2458
+ this.publicModels.set(privateModelKey(route, model.id), selection);
2459
+ if (DEEPSEEK_MODEL.test(model.id)) this.deepSeekAdapters.set(privateModelKey(route, model.id), createDeepSeekAdapter(route, profile, model, apiKeys.get(route)));
2460
+ }
2461
+ }
2462
+ requestAdapter(target) {
2463
+ return this.deepSeekAdapters.get(privateModelKey(target.route, target.model)) ?? this.piAdapter;
2464
+ }
2465
+ /** Reject routes outside the one public provider registered by this adapter. */
2466
+ assertProvider(provider) {
2467
+ if (provider !== this.provider) throw new LlmError(`chatcode-config adapter does not own provider "${provider}"`, "NO_ADAPTER");
2468
+ }
2469
+ /** Resolve the private route for one publicly selected model. */
2470
+ routeOf(provider, model) {
2471
+ this.assertProvider(provider);
2472
+ const target = this.routes.get(model);
2473
+ if (target === void 0) throw new LlmError(`chatcode-config provider has no configured model "${model}"`, "UNKNOWN_MODEL");
2474
+ return target;
2475
+ }
2476
+ validateOutput(options, target) {
2477
+ const ceiling = this.profiles.get(target.route)?.configuredMaxTokens.get(target.model);
2478
+ if (ceiling !== void 0 && options.maxTokens !== void 0 && options.maxTokens > ceiling) throw new LlmError("chatcode-config: maxTokens exceeds the configured model output limit", "UNSUPPORTED_OPTION");
2479
+ }
2480
+ providerInfo(provider) {
2481
+ this.assertProvider(provider);
2482
+ return {
2483
+ id: provider,
2484
+ name: this.providerName
2485
+ };
2486
+ }
2487
+ providerRetryPolicy(_provider) {
2488
+ return this.profiles.values().next().value?.retryPolicy;
2489
+ }
2490
+ async listModels(provider) {
2491
+ this.assertProvider(provider);
2492
+ return (await Promise.all([...this.profiles.keys()].map(async (route) => ({
2493
+ route,
2494
+ models: await this.piAdapter.listModels(route)
2495
+ })))).flatMap(({ route, models }) => models.map((model) => ({
2496
+ ...model,
2497
+ id: this.publicModels.get(privateModelKey(route, model.id)) ?? model.id,
2498
+ provider: this.provider
2499
+ })));
2500
+ }
2501
+ async resolveModel(provider, model, signal) {
2502
+ const target = this.routeOf(provider, model);
2503
+ return {
2504
+ ...await this.requestAdapter(target).resolveModel(target.route, target.model, signal),
2505
+ id: target.selection,
2506
+ provider: this.provider
2507
+ };
2508
+ }
2509
+ async prepareCall(provider, model, signal) {
2510
+ const target = this.routeOf(provider, model);
2511
+ const prepared = await this.requestAdapter(target).prepareCall(target.route, target.model, signal);
2512
+ return {
2513
+ model: {
2514
+ ...prepared.model,
2515
+ id: target.selection,
2516
+ provider: this.provider
2517
+ },
2518
+ stream: (options) => {
2519
+ const preparedTarget = this.routeOf(options.provider, options.model);
2520
+ this.validateOutput(options, preparedTarget);
2521
+ return prepared.stream({
2522
+ ...options,
2523
+ provider: preparedTarget.route,
2524
+ model: preparedTarget.model
2525
+ });
2526
+ }
2527
+ };
2528
+ }
2529
+ stream(options) {
2530
+ const target = this.routeOf(options.provider, options.model);
2531
+ this.validateOutput(options, target);
2532
+ return this.requestAdapter(target).stream({
2533
+ ...options,
2534
+ provider: target.route,
2535
+ model: target.model
2536
+ });
2537
+ }
2538
+ };
2539
+ /** Live custom-model adapter whose prepared calls retain their starting settings snapshot. */
2540
+ var LiveChatCodeAdapter = class extends LlmAdapter {
2541
+ source;
2542
+ provider;
2543
+ providerName;
2544
+ snapshot;
2545
+ constructor(source, provider = CHATCODE_PROVIDER, providerName = CHATCODE_PROVIDER_NAME) {
2546
+ super();
2547
+ this.source = source;
2548
+ this.provider = provider;
2549
+ this.providerName = providerName;
2550
+ }
2551
+ current() {
2552
+ const source = this.source();
2553
+ if (this.snapshot?.source === source) return this.snapshot.adapter;
2554
+ const adapter = new ChatCodeAdapter({
2555
+ profiles: () => source.profiles,
2556
+ resolveAuth: (route) => {
2557
+ const auth = source.auth.get(route);
2558
+ if (auth === void 0) throw new LlmError("chatcode-config: missing auth for resolved profile", "INVARIANT");
2559
+ return Promise.resolve(auth);
2560
+ },
2561
+ auth: isolatedPiAiAuth()
2562
+ }, source.profiles, this.provider, this.providerName, source.selections, source.apiKeys);
2563
+ this.snapshot = {
2564
+ source,
2565
+ adapter
2566
+ };
2567
+ return adapter;
2568
+ }
2569
+ providerInfo(provider) {
2570
+ return this.current().providerInfo(provider);
2571
+ }
2572
+ providerRetryPolicy(provider) {
2573
+ return this.current().providerRetryPolicy(provider);
2574
+ }
2575
+ listModels(provider) {
2576
+ return this.current().listModels(provider);
2577
+ }
2578
+ resolveModel(provider, model, signal) {
2579
+ return this.current().resolveModel(provider, model, signal);
2580
+ }
2581
+ prepareCall(provider, model, signal) {
2582
+ return this.current().prepareCall(provider, model, signal);
2583
+ }
2584
+ stream(options) {
2585
+ return this.current().stream(options);
2586
+ }
2587
+ };
2588
+ //#endregion
2589
+ //#region src/chatcode-auth.ts
2590
+ /** ChatCode session login and its shared, host-only credential record. */
2591
+ /** Keep the address stable for grants written by the initial implementation. */
2592
+ const CHATCODE_CREDENTIAL_KEY = credentialKey("chatcode-auth", "default");
2593
+ /** Process-scoped credential accepted from immutable desktop/terminal launchers. */
2594
+ const CHATCODE_CLI_OAUTH_TOKEN = "CHATCODE_CLI_OAUTH_TOKEN";
2595
+ const objectOf = (value) => value !== null && typeof value === "object" ? value : {};
2596
+ const stringOf = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
2597
+ const finiteTime = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 864e13;
2598
+ function endpoint(value) {
2599
+ const url = new URL(value);
2600
+ if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && [
2601
+ "localhost",
2602
+ "127.0.0.1",
2603
+ "[::1]"
2604
+ ].includes(url.hostname))) throw new Error("ChatCode endpoints require HTTPS (HTTP is allowed only on loopback).");
2605
+ return url;
2606
+ }
2607
+ /** Preserve the configured hash-router query while adding this attempt's UUID. */
2608
+ function loginUrlOf(loginUrl, sessionId) {
2609
+ const url = endpoint(loginUrl);
2610
+ if (!url.hash) url.searchParams.set("sessionId", sessionId);
2611
+ else {
2612
+ const hash = url.hash.slice(1);
2613
+ const split = hash.indexOf("?");
2614
+ const route = split < 0 ? hash : hash.slice(0, split);
2615
+ const params = new URLSearchParams(split < 0 ? "" : hash.slice(split + 1));
2616
+ params.set("sessionId", sessionId);
2617
+ url.hash = `${route}?${params.toString()}`;
2618
+ }
2619
+ return url.href;
2620
+ }
2621
+ /** Decode both the original payload and the concurrency-safe envelope. */
2622
+ function grantOf(value) {
2623
+ const envelope = objectOf(value);
2624
+ const candidate = envelope.version === 2 ? objectOf(envelope.grant) : envelope;
2625
+ if (candidate.version !== 1 || !stringOf(candidate.accessToken)) return void 0;
2626
+ for (const field of [
2627
+ "refreshToken",
2628
+ "longToken",
2629
+ "userName",
2630
+ "emailAddress"
2631
+ ]) if (candidate[field] !== void 0 && typeof candidate[field] !== "string") return void 0;
2632
+ if (candidate.expiresAtMs !== void 0 && !finiteTime(candidate.expiresAtMs)) return void 0;
2633
+ return {
2634
+ version: 1,
2635
+ accessToken: stringOf(candidate.accessToken),
2636
+ ...candidate.refreshToken === void 0 ? {} : { refreshToken: candidate.refreshToken },
2637
+ ...candidate.longToken === void 0 ? {} : { longToken: candidate.longToken },
2638
+ ...candidate.userName === void 0 ? {} : { userName: candidate.userName },
2639
+ ...candidate.emailAddress === void 0 ? {} : { emailAddress: candidate.emailAddress },
2640
+ ...candidate.expiresAtMs === void 0 ? {} : { expiresAtMs: candidate.expiresAtMs }
2641
+ };
2642
+ }
2643
+ function stateOf(record) {
2644
+ const payload = objectOf(record?.kind === "grant" ? record.payload : void 0);
2645
+ const grant = grantOf(payload);
2646
+ const state = {
2647
+ version: 2,
2648
+ ...grant ? { grant } : {}
2649
+ };
2650
+ if (payload.version !== 2) return state;
2651
+ const login = objectOf(payload.login);
2652
+ if (typeof login.sessionId === "string" && finiteTime(login.expiresAtMs) && [
2653
+ "pending",
2654
+ "succeeded",
2655
+ "failed",
2656
+ "timed-out",
2657
+ "cancelled"
2658
+ ].includes(String(login.state))) state.login = {
2659
+ sessionId: login.sessionId,
2660
+ state: login.state,
2661
+ expiresAtMs: login.expiresAtMs
2662
+ };
2663
+ if ([
2664
+ "valid",
2665
+ "invalid",
2666
+ "unavailable"
2667
+ ].includes(String(payload.validation)) && finiteTime(payload.checkedAtMs)) {
2668
+ state.validation = payload.validation;
2669
+ state.checkedAtMs = payload.checkedAtMs;
2670
+ }
2671
+ return state;
2672
+ }
2673
+ function recordOf(state) {
2674
+ return {
2675
+ kind: "grant",
2676
+ payload: { ...state }
2677
+ };
2678
+ }
2679
+ function emailOf(value) {
2680
+ const root = objectOf(value);
2681
+ const data = objectOf(root.data);
2682
+ for (const source of [
2683
+ data,
2684
+ objectOf(data.account),
2685
+ root
2686
+ ]) for (const key of [
2687
+ "email",
2688
+ "emailAddress",
2689
+ "userEmail",
2690
+ "mail"
2691
+ ]) {
2692
+ const email = stringOf(source[key]);
2693
+ if (email && email.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return email;
2694
+ }
2695
+ }
2696
+ function responseGrant(body, previous) {
2697
+ const data = objectOf(objectOf(body).data);
2698
+ const accessToken = stringOf(data.access_token);
2699
+ if (!accessToken) return void 0;
2700
+ const expiresAtMs = typeof data.expires_in === "number" && data.expires_in > 0 ? Date.now() + data.expires_in * 1e3 : void 0;
2701
+ const grant = {
2702
+ version: 1,
2703
+ accessToken
2704
+ };
2705
+ for (const [target, source] of [
2706
+ ["refreshToken", "refresh_token"],
2707
+ ["longToken", "longToken"],
2708
+ ["userName", "userName"]
2709
+ ]) {
2710
+ const value = stringOf(data[source]) ?? previous?.[target];
2711
+ if (value) grant[target] = value;
2712
+ }
2713
+ if (finiteTime(expiresAtMs)) grant.expiresAtMs = expiresAtMs;
2714
+ if (previous?.emailAddress) grant.emailAddress = previous.emailAddress;
2715
+ return grant;
2716
+ }
2717
+ function stale(grant) {
2718
+ return grant.expiresAtMs !== void 0 && grant.expiresAtMs <= Date.now() + 3e4;
2719
+ }
2720
+ /**
2721
+ * Snapshot the desktop grant only from the inherited process environment.
2722
+ * Project/user .env files are deliberately excluded: this credential is an
2723
+ * explicit property of one host launch, not durable ChatCode CLI configuration.
2724
+ */
2725
+ function chatCodeEnvironmentToken(ctx) {
2726
+ return stringOf(launchEnvironmentOf(ctx).getFrom(CHATCODE_CLI_OAUTH_TOKEN, ["process"])?.value);
2727
+ }
2728
+ /** One protocol owner, shared across Web and TUI through credentials-local's file lock. */
2729
+ var ChatCodeAuthService = class extends Service {
2730
+ credentials;
2731
+ options;
2732
+ lifetime = new AbortController();
2733
+ active = /* @__PURE__ */ new Map();
2734
+ failures = /* @__PURE__ */ new Map();
2735
+ tasks = /* @__PURE__ */ new Set();
2736
+ /** A new host process must contact ChatCode once before trusting persisted validation metadata. */
2737
+ startupValidationPending = true;
2738
+ environmentAccessToken;
2739
+ constructor(ctx, credentials, options, environmentAccessToken) {
2740
+ super(ctx, "chatcodeAuth");
2741
+ this.credentials = credentials;
2742
+ this.options = options;
2743
+ this.environmentAccessToken = stringOf(environmentAccessToken);
2744
+ endpoint(options.loginUrl);
2745
+ endpoint(options.apiBaseUrl);
2746
+ ctx.effect(() => async () => {
2747
+ this.lifetime.abort();
2748
+ await Promise.allSettled(this.tasks);
2749
+ }, "chatcode-auth: stop pending requests");
2750
+ }
2751
+ async mutate(fn) {
2752
+ try {
2753
+ return stateOf(await this.credentials.modifyRecord(CHATCODE_CREDENTIAL_KEY, async (record) => {
2754
+ const next = await fn(stateOf(record));
2755
+ return next === void 0 ? void 0 : recordOf(next);
2756
+ }));
2757
+ } catch {
2758
+ throw new Error("ChatCode credential operation failed; check the ChatCode CLI credential store permissions and availability.");
2759
+ }
2760
+ }
2761
+ async ensure(signal, force = false) {
2762
+ const combined = AbortSignal.any([
2763
+ this.lifetime.signal,
2764
+ ...signal ? [signal] : [],
2765
+ AbortSignal.timeout(Math.min(2e4, this.options.requestTimeoutMs * 3))
2766
+ ]);
2767
+ combined.throwIfAborted();
2768
+ return await this.mutate(async (current) => {
2769
+ combined.throwIfAborted();
2770
+ const grant = current.grant;
2771
+ if (!grant) {
2772
+ this.startupValidationPending = false;
2773
+ return;
2774
+ }
2775
+ if (!force && !this.startupValidationPending) return void 0;
2776
+ let next = grant;
2777
+ let result;
2778
+ try {
2779
+ if (stale(grant)) {
2780
+ const refreshed = await this.refresh(grant, combined);
2781
+ if (refreshed.grant) next = refreshed.grant;
2782
+ result = refreshed.grant ? await this.account(next, combined) : { validation: refreshed.validation };
2783
+ } else {
2784
+ result = await this.account(grant, combined);
2785
+ if (result.validation === "invalid") {
2786
+ const refreshed = await this.refresh(grant, combined);
2787
+ if (refreshed.grant) {
2788
+ next = refreshed.grant;
2789
+ result = await this.account(next, combined);
2790
+ } else result = { validation: refreshed.validation };
2791
+ }
2792
+ }
2793
+ } finally {
2794
+ this.startupValidationPending = false;
2795
+ }
2796
+ if (result.emailAddress) next = {
2797
+ ...next,
2798
+ emailAddress: result.emailAddress
2799
+ };
2800
+ signal?.throwIfAborted();
2801
+ this.lifetime.signal.throwIfAborted();
2802
+ return {
2803
+ ...current,
2804
+ grant: next,
2805
+ validation: result.validation,
2806
+ checkedAtMs: Date.now()
2807
+ };
2808
+ });
2809
+ }
2810
+ async status(options = {}) {
2811
+ if (this.environmentAccessToken !== void 0) return {
2812
+ required: this.options.requireLogin !== false,
2813
+ configured: true,
2814
+ expired: false,
2815
+ validation: "valid"
2816
+ };
2817
+ const state = await this.ensure(void 0, options.force === true);
2818
+ const grant = state.grant;
2819
+ const login = state.login?.state === "pending" ? this.failures.get(state.login.sessionId) ?? state.login : state.login;
2820
+ return {
2821
+ required: this.options.requireLogin !== false,
2822
+ configured: grant !== void 0,
2823
+ expired: grant?.expiresAtMs !== void 0 && grant.expiresAtMs <= Date.now(),
2824
+ validation: grant ? state.validation ?? "unavailable" : "none",
2825
+ ...grant?.expiresAtMs === void 0 ? {} : { expiresAtMs: grant.expiresAtMs },
2826
+ ...grant?.userName ? { userName: grant.userName } : {},
2827
+ ...grant?.emailAddress ? { emailAddress: grant.emailAddress } : {},
2828
+ ...login ? { login: login.state === "pending" && login.expiresAtMs <= Date.now() ? {
2829
+ ...login,
2830
+ state: "timed-out"
2831
+ } : login } : {}
2832
+ };
2833
+ }
2834
+ async accessToken(signal) {
2835
+ signal?.throwIfAborted();
2836
+ if (this.environmentAccessToken !== void 0) return this.environmentAccessToken;
2837
+ const state = await this.ensure(signal);
2838
+ signal?.throwIfAborted();
2839
+ return state.validation === "valid" && state.grant && (state.grant.expiresAtMs === void 0 || state.grant.expiresAtMs > Date.now()) ? state.grant.accessToken : void 0;
2840
+ }
2841
+ environmentTokenActive() {
2842
+ return this.environmentAccessToken !== void 0;
2843
+ }
2844
+ async startLogin() {
2845
+ if (this.environmentAccessToken !== void 0) throw new Error("ChatCode authentication is provided by the launch environment.");
2846
+ this.lifetime.signal.throwIfAborted();
2847
+ const sessionId = randomUUID();
2848
+ const url = loginUrlOf(this.options.loginUrl, sessionId);
2849
+ const login = {
2850
+ sessionId,
2851
+ state: "pending",
2852
+ expiresAtMs: Date.now() + this.options.pollTimeoutMs
2853
+ };
2854
+ await this.mutate(async (current) => {
2855
+ this.lifetime.signal.throwIfAborted();
2856
+ for (const controller of this.active.values()) controller.abort();
2857
+ return {
2858
+ ...current,
2859
+ login
2860
+ };
2861
+ });
2862
+ this.failures.clear();
2863
+ const controller = new AbortController();
2864
+ this.active.set(sessionId, controller);
2865
+ const signal = AbortSignal.any([
2866
+ controller.signal,
2867
+ this.lifetime.signal,
2868
+ AbortSignal.timeout(this.options.pollTimeoutMs)
2869
+ ]);
2870
+ const task = this.pollAndCommit(login, signal).catch(async () => {
2871
+ const state = Date.now() >= login.expiresAtMs ? "timed-out" : signal.aborted ? "cancelled" : "failed";
2872
+ const failed = {
2873
+ ...login,
2874
+ state
2875
+ };
2876
+ try {
2877
+ await this.finish(failed);
2878
+ } catch {
2879
+ this.failures.set(sessionId, failed);
2880
+ }
2881
+ }).finally(() => {
2882
+ this.active.delete(sessionId);
2883
+ this.tasks.delete(task);
2884
+ });
2885
+ this.tasks.add(task);
2886
+ return {
2887
+ sessionId,
2888
+ url
2889
+ };
2890
+ }
2891
+ async logout() {
2892
+ if (this.environmentAccessToken !== void 0) return;
2893
+ for (const controller of this.active.values()) controller.abort();
2894
+ await this.mutate(async (current) => ({
2895
+ version: 2,
2896
+ ...current.login ? { login: {
2897
+ ...current.login,
2898
+ state: "cancelled"
2899
+ } } : {}
2900
+ }));
2901
+ this.failures.clear();
2902
+ }
2903
+ async cancelLogin(sessionId) {
2904
+ this.active.get(sessionId)?.abort();
2905
+ await this.mutate(async (current) => current.login?.sessionId === sessionId && current.login.state === "pending" ? {
2906
+ ...current,
2907
+ login: {
2908
+ ...current.login,
2909
+ state: "cancelled"
2910
+ }
2911
+ } : void 0);
2912
+ }
2913
+ async waitForLogin(sessionId, signal) {
2914
+ const combined = AbortSignal.any([this.lifetime.signal, ...signal ? [signal] : []]);
2915
+ while (true) {
2916
+ combined.throwIfAborted();
2917
+ const status = await this.status();
2918
+ if (status.login?.sessionId !== sessionId) return "cancelled";
2919
+ if (status.login.state !== "pending") return status.login.state;
2920
+ await setTimeout$1(this.options.pollIntervalMs, void 0, { signal: combined });
2921
+ }
2922
+ }
2923
+ async finish(login) {
2924
+ await this.mutate(async (current) => current.login?.sessionId === login.sessionId && current.login.state === "pending" ? {
2925
+ ...current,
2926
+ login
2927
+ } : void 0);
2928
+ }
2929
+ async pollAndCommit(login, signal) {
2930
+ let grant;
2931
+ while (Date.now() < login.expiresAtMs) {
2932
+ signal.throwIfAborted();
2933
+ const current = await this.mutate(async () => void 0);
2934
+ if (current.login?.sessionId !== login.sessionId || current.login.state !== "pending") return;
2935
+ if (!grant) {
2936
+ const response = await this.request(`/caassist-api-lt/caassist/api/account/session/login?${new URLSearchParams({ sessionId: login.sessionId })}`, {
2937
+ method: "GET",
2938
+ signal
2939
+ });
2940
+ if (response?.ok) grant = responseGrant(response.body);
2941
+ }
2942
+ if (grant) {
2943
+ const account = await this.account(grant, signal);
2944
+ if (account.validation === "invalid") throw new Error("ChatCode rejected the login grant.");
2945
+ if (account.validation === "valid") {
2946
+ const validated = {
2947
+ ...grant,
2948
+ ...account.emailAddress ? { emailAddress: account.emailAddress } : {}
2949
+ };
2950
+ await this.mutate(async (latest) => {
2951
+ signal.throwIfAborted();
2952
+ if (latest.login?.sessionId !== login.sessionId || latest.login.state !== "pending") return void 0;
2953
+ return {
2954
+ version: 2,
2955
+ grant: validated,
2956
+ validation: "valid",
2957
+ checkedAtMs: Date.now(),
2958
+ login: {
2959
+ ...login,
2960
+ state: "succeeded"
2961
+ }
2962
+ };
2963
+ });
2964
+ return;
2965
+ }
2966
+ }
2967
+ await setTimeout$1(Math.min(this.options.pollIntervalMs, Math.max(1, login.expiresAtMs - Date.now())), void 0, { signal });
2968
+ }
2969
+ await this.finish({
2970
+ ...login,
2971
+ state: "timed-out"
2972
+ });
2973
+ }
2974
+ async account(grant, signal) {
2975
+ const response = await this.request("/caassist-api-lt/caassist/api/account/info", {
2976
+ method: "POST",
2977
+ headers: {
2978
+ authorization: `Bearer ${grant.accessToken}`,
2979
+ "content-type": "application/json"
2980
+ },
2981
+ body: "{}",
2982
+ signal
2983
+ });
2984
+ const body = objectOf(response?.body);
2985
+ if (response?.status === 401 || response?.status === 403 || body.code === "76021501") return { validation: "invalid" };
2986
+ if (!response?.ok || !(body.success === true || body.code === "00000000")) return { validation: "unavailable" };
2987
+ const emailAddress = emailOf(body);
2988
+ return {
2989
+ validation: "valid",
2990
+ ...emailAddress ? { emailAddress } : {}
2991
+ };
2992
+ }
2993
+ async refresh(current, signal) {
2994
+ const username = current.userName || current.emailAddress;
2995
+ if (!username || !current.longToken) return { validation: "invalid" };
2996
+ const response = await this.request("/caassist-api-lt/caassist/api/account/long/login", {
2997
+ method: "POST",
2998
+ headers: { "content-type": "application/json" },
2999
+ body: JSON.stringify({
3000
+ username,
3001
+ longToken: current.longToken
3002
+ }),
3003
+ signal
3004
+ });
3005
+ const body = objectOf(response?.body);
3006
+ const grant = response?.ok ? responseGrant(body, current) : void 0;
3007
+ if (grant) return {
3008
+ grant,
3009
+ validation: "unavailable"
3010
+ };
3011
+ return { validation: response?.status === 401 || response?.status === 403 || body.code === "76021501" || body.success === false ? "invalid" : "unavailable" };
3012
+ }
3013
+ async request(path, init) {
3014
+ const signal = AbortSignal.any([
3015
+ this.lifetime.signal,
3016
+ ...init.signal ? [init.signal] : [],
3017
+ AbortSignal.timeout(this.options.requestTimeoutMs)
3018
+ ]);
3019
+ try {
3020
+ const response = await fetch(new URL(path, this.options.apiBaseUrl), {
3021
+ ...init,
3022
+ signal,
3023
+ redirect: "error"
3024
+ });
3025
+ return {
3026
+ ok: response.ok,
3027
+ status: response.status,
3028
+ body: await response.json().catch(() => void 0)
3029
+ };
3030
+ } catch {
3031
+ return;
3032
+ }
3033
+ }
3034
+ };
3035
+ //#endregion
3036
+ //#region src/config.ts
3037
+ /** Mount and settings configuration for ChatCode model sources. @module dsh-llm-chatcode-config/config */
3038
+ /** Validate mounting options without exposing control-plane credentials. */
3039
+ const Config = z.object({
3040
+ settingsPath: z.string(),
3041
+ customModels: z.array(z.object({
3042
+ model: z.string().required(),
3043
+ apiKey: z.string().required().role("secret"),
3044
+ baseUrl: z.string().required(),
3045
+ description: z.string(),
3046
+ provider: z.union([z.string(), z.const(null)]),
3047
+ protocol: z.string(),
3048
+ contextWindow: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
3049
+ maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
3050
+ maxInputTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)
3051
+ })).default([]),
3052
+ defaultContextWindow: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(262144),
3053
+ defaultMaxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(4096),
3054
+ retryPolicy: RetryPolicySchema,
3055
+ auth: z.object({
3056
+ requireLogin: z.boolean().default(true),
3057
+ loginUrl: z.string().default("https://chatcode.chinaunicom.cn/unicode/#/login"),
3058
+ apiBaseUrl: z.string().default("https://chatcode.chinaunicom.cn"),
3059
+ pollIntervalMs: z.number().step(1).min(250).max(6e4).default(1e3),
3060
+ pollTimeoutMs: z.number().step(1).min(1e3).max(72e5).default(36e5),
3061
+ requestTimeoutMs: z.number().step(1).min(1e3).max(6e4).default(5e3)
3062
+ }).default({
3063
+ requireLogin: true,
3064
+ loginUrl: "https://chatcode.chinaunicom.cn/unicode/#/login",
3065
+ apiBaseUrl: "https://chatcode.chinaunicom.cn",
3066
+ pollIntervalMs: 1e3,
3067
+ pollTimeoutMs: 36e5,
3068
+ requestTimeoutMs: 5e3
3069
+ }),
3070
+ cvpChatCodeApiUrl: z.string().default("https://chatcode.chinaunicom.cn/cvp"),
3071
+ startupGate: z.object({
3072
+ token: z.string().role("secret").default(""),
3073
+ timeoutMs: z.number().step(1).min(1e3).max(6e4).default(5e3)
3074
+ }).default({
3075
+ token: "",
3076
+ timeoutMs: 5e3
3077
+ }),
3078
+ reporting: z.object({
3079
+ enabled: z.boolean().default(true),
3080
+ codeSave: z.boolean().default(true),
3081
+ conversationSync: z.boolean().default(true),
3082
+ chatCodeSession: z.boolean().default(true),
3083
+ codeBatchItems: z.number().step(1).min(1).max(500).default(50),
3084
+ codeBatchChars: z.number().step(1).min(1024).max(5242880).default(524288),
3085
+ codeRetryDelayMs: z.number().step(1).min(1e3).max(3e5).default(5e3),
3086
+ codeOutboxDir: z.string().default(""),
3087
+ includeSubagentConversationSync: z.boolean().default(false),
3088
+ modelKindRules: z.array(z.object({
3089
+ provider: z.string(),
3090
+ model: z.string(),
3091
+ kind: z.number().step(1).min(0).max(3).required()
3092
+ })).default([])
3093
+ }).default({
3094
+ enabled: true,
3095
+ codeSave: true,
3096
+ conversationSync: true,
3097
+ chatCodeSession: true,
3098
+ codeBatchItems: 50,
3099
+ codeBatchChars: 524288,
3100
+ codeRetryDelayMs: 5e3,
3101
+ codeOutboxDir: "",
3102
+ includeSubagentConversationSync: false,
3103
+ modelKindRules: []
3104
+ }),
3105
+ codingPlanEndpoint: z.string().default("https://chatcode.chinaunicom.cn/cvp/api/cli/v1/model-runtime-configs"),
3106
+ enableMaas: z.boolean().default(false),
3107
+ maasEndpoint: z.string().default("https://chatcode.chinaunicom.cn/cvp/wanma/api/v1/cli/maas-models"),
3108
+ catalogTimeoutMs: z.number().step(1).min(1).max(6e4).default(1e4)
3109
+ });
3110
+ //#endregion
3111
+ //#region src/managed.ts
3112
+ /** Read centrally managed model catalogs without exposing their credentials. @module dsh-llm-chatcode-config/managed */
3113
+ /** Resolve the OS account that launched the current DSH host. */
3114
+ function currentUserName() {
3115
+ const environmentName = process.env.USERNAME?.trim() || process.env.USER?.trim();
3116
+ if (environmentName !== void 0 && environmentName !== "") return environmentName;
3117
+ try {
3118
+ return userInfo().username.trim();
3119
+ } catch {
3120
+ return "";
3121
+ }
3122
+ }
3123
+ /** Query fields required by the CodingPlan runtime catalogue. */
3124
+ function runtimeQuery(userName = currentUserName()) {
3125
+ return { userEmail: userName };
3126
+ }
3127
+ function record(value) {
3128
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3129
+ }
3130
+ function string(value) {
3131
+ return typeof value === "string" ? value : void 0;
3132
+ }
3133
+ function positive(value) {
3134
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
3135
+ }
3136
+ const DIAGNOSTIC_BODY_LIMIT = 16e3;
3137
+ const SENSITIVE_FIELD = /(?:api[-_]?key|access[-_]?token|refresh[-_]?token|long[-_]?token|authorization|password|secret)/i;
3138
+ /** Render enough of a JSON response to diagnose its shape without logging credentials. */
3139
+ function diagnosticBody(value) {
3140
+ try {
3141
+ const rendered = JSON.stringify(value, (key, item) => SENSITIVE_FIELD.test(key) ? "[REDACTED]" : item);
3142
+ if (rendered === void 0) return "<empty>";
3143
+ return rendered.length <= DIAGNOSTIC_BODY_LIMIT ? rendered : `${rendered.slice(0, DIAGNOSTIC_BODY_LIMIT)}...<truncated>`;
3144
+ } catch {
3145
+ return "<unserializable JSON>";
3146
+ }
3147
+ }
3148
+ function catalogHeaders(authorization, noCache = false) {
3149
+ const accessToken = authorization?.accessToken.trim();
3150
+ if (accessToken !== void 0 && accessToken !== "" && !/[\r\n]/.test(accessToken)) return {
3151
+ Accept: "application/json",
3152
+ ...noCache ? { "Cache-Control": "no-cache" } : {},
3153
+ Authorization: `Bearer ${accessToken}`,
3154
+ accessToken
3155
+ };
3156
+ return {
3157
+ Accept: "application/json",
3158
+ ...noCache ? { "Cache-Control": "no-cache" } : {}
3159
+ };
3160
+ }
3161
+ function protocolOf$1(model) {
3162
+ switch (model.protocol?.trim().toLowerCase()) {
3163
+ case "openai": return "openai-completions";
3164
+ case "anthropic": return "anthropic-messages";
3165
+ default: return;
3166
+ }
3167
+ }
3168
+ /** Accept gateway roots and a complete OpenAI chat-completions URL from the control plane. */
3169
+ function endpointOf$1(value) {
3170
+ let url;
3171
+ try {
3172
+ url = new URL(value);
3173
+ } catch {
3174
+ return;
3175
+ }
3176
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) return void 0;
3177
+ url.pathname = url.pathname.replace(/\/chat\/completions\/?$/i, "") || "/";
3178
+ return url.href.replace(/\/+$/, "");
3179
+ }
3180
+ function requestApiKey(apiKey) {
3181
+ if (apiKey === void 0 || apiKey.trim() === "" || /^\*+$/.test(apiKey.trim()) || /[\r\n]/.test(apiKey)) return void 0;
3182
+ return apiKey;
3183
+ }
3184
+ function requestAuth(protocol, apiKey) {
3185
+ if (apiKey === void 0) return {};
3186
+ return protocol === "anthropic-messages" ? { headers: { Authorization: `Bearer ${apiKey}` } } : { apiKey };
3187
+ }
3188
+ /** Fetch the supported control-plane response shape and report only generic, non-secret errors. */
3189
+ async function fetchRuntimeModels(endpoint, timeoutMs, authorization, diagnostic) {
3190
+ let url;
3191
+ try {
3192
+ url = new URL(endpoint);
3193
+ } catch {
3194
+ throw new LlmError("chatcode-config: managed model endpoint is invalid", "INVALID_CHATCODE_CONFIG");
3195
+ }
3196
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new LlmError("chatcode-config: managed model endpoint must be HTTP(S) without credentials", "INVALID_CHATCODE_CONFIG");
3197
+ for (const [key, value] of Object.entries(runtimeQuery())) url.searchParams.set(key, value);
3198
+ diagnostic?.(`chatcode-config: CodingPlan request GET ${url.href}`);
3199
+ let response;
3200
+ try {
3201
+ response = await fetch(url, {
3202
+ headers: catalogHeaders(authorization),
3203
+ signal: AbortSignal.timeout(timeoutMs)
3204
+ });
3205
+ } catch (error) {
3206
+ diagnostic?.(`chatcode-config: CodingPlan request failed before a response (${error instanceof Error ? error.message : String(error)})`);
3207
+ throw new LlmError("chatcode-config: managed model catalog is unavailable", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3208
+ }
3209
+ let body;
3210
+ try {
3211
+ body = await response.json();
3212
+ diagnostic?.(`chatcode-config: CodingPlan response status=${response.status} body=${diagnosticBody(body)}`);
3213
+ } catch {
3214
+ diagnostic?.(`chatcode-config: CodingPlan response status=${response.status} body=<invalid JSON>`);
3215
+ throw new LlmError("chatcode-config: managed model catalog returned invalid JSON", "MANAGED_MODEL_CATALOG_INVALID");
3216
+ }
3217
+ if (!response.ok) throw new LlmError("chatcode-config: managed model catalog request failed", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3218
+ const top = record(body);
3219
+ const data = top === void 0 ? void 0 : record(top.data);
3220
+ const rows = data === void 0 ? void 0 : data.models;
3221
+ if (top?.code !== "00000000" || !Array.isArray(rows)) throw new LlmError("chatcode-config: managed model catalog returned an invalid response", "MANAGED_MODEL_CATALOG_INVALID");
3222
+ const models = rows.flatMap((row) => {
3223
+ const item = record(row);
3224
+ if (item === void 0) return [];
3225
+ const logicalModelId = string(item.logicalModelId);
3226
+ if (logicalModelId === void 0 || logicalModelId.trim() === "") return [];
3227
+ const parsed = {
3228
+ logicalModelId,
3229
+ displayName: string(item.displayName) ?? logicalModelId
3230
+ };
3231
+ const description = string(item.description);
3232
+ const protocol = string(item.protocol);
3233
+ const provider = string(item.provider);
3234
+ const baseUrl = string(item.baseUrl);
3235
+ const providerModelId = string(item.providerModelId);
3236
+ const apiKey = string(item.apiKey);
3237
+ const maxToken = positive(item.maxToken);
3238
+ const contextWindow = positive(item.contextWindow);
3239
+ if (description !== void 0) parsed.description = description;
3240
+ if (protocol !== void 0) parsed.protocol = protocol;
3241
+ if (provider !== void 0) parsed.provider = provider;
3242
+ if (baseUrl !== void 0) parsed.baseUrl = baseUrl;
3243
+ if (providerModelId !== void 0) parsed.providerModelId = providerModelId;
3244
+ if (apiKey !== void 0) parsed.apiKey = apiKey;
3245
+ if (maxToken !== void 0) parsed.maxToken = maxToken;
3246
+ if (contextWindow !== void 0) parsed.contextWindow = contextWindow;
3247
+ return [parsed];
3248
+ });
3249
+ diagnostic?.(`chatcode-config: CodingPlan parsed ${String(rows.length)} response rows into ${String(models.length)} model records`);
3250
+ return models;
3251
+ }
3252
+ /** Fetch the public MAAS catalog. It intentionally contains no endpoint or credential fields. */
3253
+ async function fetchMaasCatalog(endpoint, timeoutMs, authorization) {
3254
+ let url;
3255
+ try {
3256
+ url = new URL(endpoint);
3257
+ } catch {
3258
+ throw new LlmError("chatcode-config: MAAS model endpoint is invalid", "INVALID_CHATCODE_CONFIG");
3259
+ }
3260
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new LlmError("chatcode-config: MAAS model endpoint must be HTTP(S) without credentials", "INVALID_CHATCODE_CONFIG");
3261
+ let response;
3262
+ try {
3263
+ response = await fetch(url, {
3264
+ headers: catalogHeaders(authorization, true),
3265
+ signal: AbortSignal.timeout(timeoutMs)
3266
+ });
3267
+ } catch {
3268
+ throw new LlmError("chatcode-config: MAAS model catalog is unavailable", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3269
+ }
3270
+ if (!response.ok) throw new LlmError("chatcode-config: MAAS model catalog request failed", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3271
+ let body;
3272
+ try {
3273
+ body = await response.json();
3274
+ } catch {
3275
+ throw new LlmError("chatcode-config: MAAS model catalog returned invalid JSON", "MANAGED_MODEL_CATALOG_INVALID");
3276
+ }
3277
+ const top = record(body);
3278
+ if (top?.code !== 200 || !Array.isArray(top.data)) throw new LlmError("chatcode-config: MAAS model catalog returned an invalid response", "MANAGED_MODEL_CATALOG_INVALID");
3279
+ return top.data.flatMap((row) => {
3280
+ const item = record(row);
3281
+ const id = positive(item?.id);
3282
+ const logicalModelId = string(item?.logicalModelId)?.trim();
3283
+ if (id === void 0 || logicalModelId === void 0 || logicalModelId === "") return [];
3284
+ const parsed = {
3285
+ id,
3286
+ logicalModelId,
3287
+ displayName: string(item?.displayName)?.trim() || logicalModelId
3288
+ };
3289
+ const description = string(item?.description);
3290
+ const protocol = string(item?.protocol);
3291
+ const model = string(item?.model);
3292
+ const maxTokens = positive(item?.maxTokens);
3293
+ const contextWindow = positive(item?.contextWindow);
3294
+ if (description !== void 0) parsed.description = description;
3295
+ if (protocol !== void 0) parsed.protocol = protocol;
3296
+ if (model !== void 0) parsed.model = model;
3297
+ if (maxTokens !== void 0) parsed.maxTokens = maxTokens;
3298
+ if (contextWindow !== void 0) parsed.contextWindow = contextWindow;
3299
+ return [parsed];
3300
+ });
3301
+ }
3302
+ function maasRuntimeUrl(catalogEndpoint, id) {
3303
+ let url;
3304
+ try {
3305
+ url = new URL(catalogEndpoint);
3306
+ } catch {
3307
+ throw new LlmError("chatcode-config: MAAS model endpoint is invalid", "INVALID_CHATCODE_CONFIG");
3308
+ }
3309
+ const path = url.pathname.replace(/\/+$/, "");
3310
+ if (!path.endsWith("/maas-models")) throw new LlmError("chatcode-config: MAAS endpoint must end with /maas-models", "INVALID_CHATCODE_CONFIG");
3311
+ url.pathname = `${path}/${String(id)}/runtime-config`;
3312
+ url.search = "";
3313
+ return url;
3314
+ }
3315
+ /** Fetch one selected MAAS model's private runtime configuration. */
3316
+ async function fetchMaasRuntime(endpoint, catalog, timeoutMs, authorization) {
3317
+ const url = maasRuntimeUrl(endpoint, catalog.id);
3318
+ let response;
3319
+ try {
3320
+ response = await fetch(url, {
3321
+ headers: catalogHeaders(authorization, true),
3322
+ signal: AbortSignal.timeout(timeoutMs)
3323
+ });
3324
+ } catch {
3325
+ throw new LlmError("chatcode-config: MAAS runtime configuration is unavailable", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3326
+ }
3327
+ if (!response.ok) throw new LlmError("chatcode-config: MAAS runtime configuration request failed", "MANAGED_MODEL_CATALOG_UNAVAILABLE");
3328
+ let body;
3329
+ try {
3330
+ body = await response.json();
3331
+ } catch {
3332
+ throw new LlmError("chatcode-config: MAAS runtime configuration returned invalid JSON", "MANAGED_MODEL_CATALOG_INVALID");
3333
+ }
3334
+ const top = record(body);
3335
+ const data = top === void 0 ? void 0 : record(top.data);
3336
+ const runtimeId = positive(data?.id);
3337
+ if (top?.code !== 200 || data === void 0 || runtimeId !== void 0 && runtimeId !== catalog.id) throw new LlmError("chatcode-config: MAAS runtime configuration returned an invalid response", "MANAGED_MODEL_CATALOG_INVALID");
3338
+ const logicalModelId = string(data.logicalModelId)?.trim() || catalog.logicalModelId;
3339
+ const providerModelId = string(data.model)?.trim() || catalog.model?.trim();
3340
+ const baseUrl = string(data.baseUrl);
3341
+ if (logicalModelId === void 0 || logicalModelId === "" || providerModelId === void 0 || providerModelId === "" || baseUrl === void 0) return void 0;
3342
+ const parsed = {
3343
+ logicalModelId,
3344
+ displayName: string(data.displayName)?.trim() || catalog.displayName,
3345
+ baseUrl,
3346
+ providerModelId
3347
+ };
3348
+ const description = string(data.description) ?? catalog.description;
3349
+ const protocol = string(data.protocol) ?? catalog.protocol;
3350
+ const apiKey = string(data.apiKey);
3351
+ const maxToken = positive(data.maxTokens) ?? catalog.maxTokens;
3352
+ const contextWindow = positive(data.contextWindow) ?? catalog.contextWindow;
3353
+ if (description !== void 0) parsed.description = description;
3354
+ if (protocol !== void 0) parsed.protocol = protocol;
3355
+ if (apiKey !== void 0) parsed.apiKey = apiKey;
3356
+ if (maxToken !== void 0) parsed.maxToken = maxToken;
3357
+ if (contextWindow !== void 0) parsed.contextWindow = contextWindow;
3358
+ return parsed;
3359
+ }
3360
+ /** Resolve every public MAAS entry through its private runtime endpoint before publishing it as selectable. */
3361
+ async function fetchMaasRuntimeModels(endpoint, timeoutMs, authorization) {
3362
+ const catalog = await fetchMaasCatalog(endpoint, timeoutMs, authorization);
3363
+ return (await Promise.all(catalog.map(async (item) => {
3364
+ try {
3365
+ return await fetchMaasRuntime(endpoint, item, timeoutMs, authorization);
3366
+ } catch (error) {
3367
+ if (error instanceof LlmError) return void 0;
3368
+ throw error;
3369
+ }
3370
+ }))).flatMap((model) => model === void 0 ? [] : [model]);
3371
+ }
3372
+ /** Translate runnable managed entries into detached pi-ai routes. Incomplete public metadata is deliberately not selectable. */
3373
+ function resolveManagedSource(models, group, config) {
3374
+ const profiles = {};
3375
+ const auth = /* @__PURE__ */ new Map();
3376
+ const apiKeys = /* @__PURE__ */ new Map();
3377
+ const selections = /* @__PURE__ */ new Map();
3378
+ for (const entry of models) {
3379
+ const protocol = protocolOf$1(entry);
3380
+ const baseURL = entry.baseUrl === void 0 ? void 0 : endpointOf$1(entry.baseUrl);
3381
+ const providerModelId = entry.providerModelId?.trim();
3382
+ if (protocol === void 0 || baseURL === void 0 || providerModelId === void 0 || providerModelId === "") continue;
3383
+ const selection = entry.logicalModelId.trim();
3384
+ if (selection === "" || selections.has(selection)) continue;
3385
+ const route = `managed-${group}-${createHash("sha256").update(JSON.stringify([
3386
+ protocol,
3387
+ baseURL,
3388
+ providerModelId,
3389
+ selection
3390
+ ])).digest("hex").slice(0, 20)}`;
3391
+ profiles[route] = {
3392
+ displayName: entry.displayName.trim() || selection,
3393
+ api: protocol,
3394
+ baseURL,
3395
+ models: [{
3396
+ id: providerModelId,
3397
+ name: entry.displayName.trim() || selection,
3398
+ contextWindow: entry.contextWindow ?? entry.maxToken ?? config.defaultContextWindow,
3399
+ maxTokens: entry.maxToken ?? config.defaultMaxTokens
3400
+ }],
3401
+ ...protocol === "openai-completions" ? {
3402
+ compat: {
3403
+ maxTokensField: "max_tokens",
3404
+ supportsDeveloperRole: false
3405
+ },
3406
+ headers: { Accept: "text/event-stream" }
3407
+ } : {},
3408
+ ...config.retryPolicy === void 0 ? {} : { retryPolicy: config.retryPolicy }
3409
+ };
3410
+ const apiKey = requestApiKey(entry.apiKey);
3411
+ auth.set(route, requestAuth(protocol, apiKey));
3412
+ if (apiKey !== void 0) apiKeys.set(route, apiKey);
3413
+ selections.set(selection, {
3414
+ route,
3415
+ model: providerModelId
3416
+ });
3417
+ }
3418
+ return {
3419
+ profiles: resolveProfiles(profiles),
3420
+ auth,
3421
+ apiKeys,
3422
+ selections
3423
+ };
3424
+ }
3425
+ //#endregion
3426
+ //#region src/reporting/outbox.ts
3427
+ /** Durable file-per-record outbox for generated-code statistics. */
3428
+ /** Persist generated code until a backend acknowledgement permits deletion. */
3429
+ var CodeOutbox = class {
3430
+ directory;
3431
+ constructor(directory) {
3432
+ this.directory = directory;
3433
+ }
3434
+ /** Atomically append non-empty code strings to the outbox. */
3435
+ async enqueue(codes) {
3436
+ const kept = codes.map((code) => code.trim()).filter(Boolean);
3437
+ if (kept.length === 0) return;
3438
+ await mkdir(this.directory, {
3439
+ recursive: true,
3440
+ mode: 448
3441
+ });
3442
+ for (const code of kept) await this.writeRecord(code);
3443
+ }
3444
+ async writeRecord(code) {
3445
+ const id = randomUUID();
3446
+ const record = {
3447
+ version: 1,
3448
+ id,
3449
+ createdAt: Date.now(),
3450
+ codes: [code]
3451
+ };
3452
+ const target = join(this.directory, `${String(record.createdAt).padStart(13, "0")}-${id}.json`);
3453
+ const temporary = `${target}.${randomUUID()}.tmp`;
3454
+ const handle = await open(temporary, "wx", 384);
3455
+ try {
3456
+ await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
3457
+ await handle.sync();
3458
+ } finally {
3459
+ await handle.close();
3460
+ }
3461
+ await rename(temporary, target);
3462
+ }
3463
+ /** Read the oldest complete records within both configured batch limits. */
3464
+ async readBatch(maxItems, maxChars) {
3465
+ await mkdir(this.directory, {
3466
+ recursive: true,
3467
+ mode: 448
3468
+ });
3469
+ const entries = (await readdir(this.directory, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort();
3470
+ const batch = {
3471
+ files: [],
3472
+ codes: []
3473
+ };
3474
+ let chars = 0;
3475
+ for (const name of entries) {
3476
+ const path = join(this.directory, name);
3477
+ const record = await this.readRecord(path);
3478
+ if (!record) continue;
3479
+ const nextChars = record.codes.reduce((sum, code) => sum + code.length, 0);
3480
+ if (batch.codes.length > 0 && (batch.codes.length + record.codes.length > maxItems || chars + nextChars > maxChars)) break;
3481
+ batch.files.push(path);
3482
+ batch.codes.push(...record.codes);
3483
+ chars += nextChars;
3484
+ if (batch.codes.length >= maxItems || chars >= maxChars) break;
3485
+ }
3486
+ return batch;
3487
+ }
3488
+ /** Delete only records included in a backend-acknowledged batch. */
3489
+ async acknowledge(files) {
3490
+ for (const file of files) try {
3491
+ await unlink(file);
3492
+ } catch (error) {
3493
+ if (error.code !== "ENOENT") throw error;
3494
+ }
3495
+ }
3496
+ async readRecord(path) {
3497
+ try {
3498
+ const value = JSON.parse(await readFile(path, "utf8"));
3499
+ if (isRecord$2(value) && value.version === 1 && typeof value.id === "string" && Number.isSafeInteger(value.createdAt) && Array.isArray(value.codes) && value.codes.length > 0 && value.codes.every((code) => typeof code === "string" && code.trim().length > 0)) return value;
3500
+ } catch (error) {
3501
+ if (error.code === "ENOENT") return void 0;
3502
+ }
3503
+ const invalidDirectory = join(this.directory, "invalid");
3504
+ await mkdir(invalidDirectory, {
3505
+ recursive: true,
3506
+ mode: 448
3507
+ });
3508
+ await rename(path, join(invalidDirectory, `${basename(path)}.${randomUUID()}.invalid`));
3509
+ }
3510
+ };
3511
+ function isRecord$2(value) {
3512
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3513
+ }
3514
+ //#endregion
3515
+ //#region src/reporting/code.ts
3516
+ /** Pure extraction of code that is safe to count as AI-generated output. */
3517
+ const EXCLUDED_FENCE_LABELS = /* @__PURE__ */ new Set([
3518
+ "text",
3519
+ "plaintext",
3520
+ "plain",
3521
+ "output",
3522
+ "result",
3523
+ "log",
3524
+ "example",
3525
+ "sample",
3526
+ "demo",
3527
+ "note",
3528
+ "notice",
3529
+ "warning",
3530
+ "error",
3531
+ "exception",
3532
+ "stacktrace",
3533
+ "trace"
3534
+ ]);
3535
+ /** Extract reportable fenced code blocks from assistant-visible Markdown. */
3536
+ function extractMarkdownCode(markdown) {
3537
+ const result = [];
3538
+ for (const match of markdown.matchAll(/(?:^|\n)(`{3,}|~{3,})[ \t]*([^\r\n]*)\r?\n([\s\S]*?)\r?\n?\1(?=\r?\n|$)/g)) {
3539
+ const label = match[2]?.trim().split(/[ \t]/, 1)[0]?.toLowerCase() ?? "";
3540
+ const code = match[3]?.trim();
3541
+ if (!code || EXCLUDED_FENCE_LABELS.has(label) || !label && looksLikeConsoleResult(code)) continue;
3542
+ result.push({
3543
+ code,
3544
+ ...label ? { language: label } : {}
3545
+ });
3546
+ }
3547
+ return result;
3548
+ }
3549
+ /** Extract code from one successful first-party mutation request. */
3550
+ function extractMutationCode(name, args) {
3551
+ if (!isRecord$1(args)) return void 0;
3552
+ if (name === "write") return codeAt(args, "file_path", "content");
3553
+ if (name === "edit") {
3554
+ if (typeof args.old_string !== "string" || args.old_string.length === 0) return void 0;
3555
+ return codeAt(args, "file_path", "new_string");
3556
+ }
3557
+ if (name !== "str_replace_editor") return void 0;
3558
+ if (args.command === "create") return codeAt(args, "path", "file_text");
3559
+ if (args.command === "str_replace") {
3560
+ if (typeof args.old_str !== "string" || args.old_str.length === 0) return void 0;
3561
+ return codeAt(args, "path", "new_str");
3562
+ }
3563
+ if (args.command === "insert" && Number.isInteger(args.insert_line)) return codeAt(args, "path", "new_str");
3564
+ }
3565
+ /** Parse model-produced JSON arguments before mutation extraction. */
3566
+ function extractMutationCodeFromJson(name, raw) {
3567
+ try {
3568
+ return extractMutationCode(name, JSON.parse(raw));
3569
+ } catch {
3570
+ return;
3571
+ }
3572
+ }
3573
+ /** Infer a stable Markdown language name from a file path. */
3574
+ function inferLanguage(filePath) {
3575
+ const extension = extname(filePath).slice(1).toLowerCase();
3576
+ if (!extension) return void 0;
3577
+ return {
3578
+ cjs: "javascript",
3579
+ htm: "html",
3580
+ js: "javascript",
3581
+ mjs: "javascript",
3582
+ py: "python",
3583
+ ps1: "powershell",
3584
+ sh: "bash",
3585
+ ts: "typescript",
3586
+ yml: "yaml"
3587
+ }[extension] ?? extension;
3588
+ }
3589
+ function codeAt(args, pathKey, codeKey) {
3590
+ const code = args[codeKey];
3591
+ if (typeof code !== "string" || code.trim().length === 0) return void 0;
3592
+ const filePath = args[pathKey];
3593
+ if (typeof filePath !== "string" || filePath.trim().length === 0) return { code: code.trim() };
3594
+ const language = inferLanguage(filePath);
3595
+ return {
3596
+ code: code.trim(),
3597
+ filePath,
3598
+ ...language ? { language } : {}
3599
+ };
3600
+ }
3601
+ function looksLikeConsoleResult(code) {
3602
+ const lines = code.split("\n");
3603
+ if (lines.length <= 3) return false;
3604
+ return lines.filter((line) => /^[A-Za-z_]\w*\(\d+\)\s*=/.test(line.trim())).length > lines.length * .5;
3605
+ }
3606
+ function isRecord$1(value) {
3607
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3608
+ }
3609
+ //#endregion
3610
+ //#region src/reporting/payloads.ts
3611
+ /** Return the first explicit model-kind override, if one matches. */
3612
+ function matchModelKindRule(provider, model, rules) {
3613
+ return rules.find((rule) => (rule.provider === void 0 || rule.provider === provider) && (rule.model === void 0 || rule.model === model))?.kind;
3614
+ }
3615
+ /** Build the immutable conversation fields for one Session. */
3616
+ function conversationMeta(session, userEmail, title, modelName) {
3617
+ return {
3618
+ sessionId: String(session.id),
3619
+ userEmail,
3620
+ projectPath: session.header.cwd ?? "",
3621
+ ...title ? { title } : {},
3622
+ modelName,
3623
+ startTime: new Date(session.header.createdAt).toISOString()
3624
+ };
3625
+ }
3626
+ /** Project one human or assistant message into a single backend row. */
3627
+ function conversationMessage(sessionId, message, timestamp, turn, usage) {
3628
+ const projected = projectBlocks(message.content);
3629
+ if (!projected) return void 0;
3630
+ const model = message.source.kind === "model" ? message.source.model : void 0;
3631
+ return {
3632
+ uuid: String(message.id),
3633
+ sessionId,
3634
+ role: message.role === "assistant" ? "assistant" : message.role === "system" ? "system" : "user",
3635
+ contentType: projected.contentType,
3636
+ content: projected.content,
3637
+ ...model ? { modelName: model } : {},
3638
+ ...usage ? { tokenUsage: {
3639
+ inputTokens: usage.inputTokens,
3640
+ outputTokens: usage.outputTokens,
3641
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
3642
+ cacheCreationTokens: usage.cacheWriteTokens ?? 0
3643
+ } } : {},
3644
+ usageTime: new Date(timestamp).toISOString(),
3645
+ timestamp: new Date(timestamp).toISOString(),
3646
+ turnIndex: turn
3647
+ };
3648
+ }
3649
+ /** Project one durable tool call into a stable conversation row. */
3650
+ function toolCallMessage(sessionId, callId, name, args, timestamp, turn) {
3651
+ return {
3652
+ uuid: callId,
3653
+ sessionId,
3654
+ role: "assistant",
3655
+ contentType: "tool_use",
3656
+ content: name,
3657
+ toolName: name,
3658
+ toolInput: args,
3659
+ usageTime: new Date(timestamp).toISOString(),
3660
+ timestamp: new Date(timestamp).toISOString(),
3661
+ turnIndex: turn
3662
+ };
3663
+ }
3664
+ /** Project one durable tool result into a stable conversation row. */
3665
+ function toolResultMessage(sessionId, message, name, timestamp, turn) {
3666
+ return {
3667
+ uuid: String(message.id),
3668
+ sessionId,
3669
+ role: "tool",
3670
+ contentType: "tool_result",
3671
+ content: textOfBlocks(message.content),
3672
+ toolName: name,
3673
+ usageTime: new Date(timestamp).toISOString(),
3674
+ timestamp: new Date(timestamp).toISOString(),
3675
+ turnIndex: turn
3676
+ };
3677
+ }
3678
+ /** Return assistant-visible text without reasoning or tool-call arguments. */
3679
+ function visibleText(message) {
3680
+ return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
3681
+ }
3682
+ /** Return direct-user text suitable for a title or question. */
3683
+ function userText(message) {
3684
+ return message.content.map((block) => {
3685
+ if (block.type === "text") return block.text;
3686
+ if (block.type === "image") return "[image]";
3687
+ if (block.type === "file") return `[file: ${block.attachment.name}]`;
3688
+ return "";
3689
+ }).filter(Boolean).join("\n").trim();
3690
+ }
3691
+ /** Return a bounded, one-line tool result summary. */
3692
+ function toolSummary(message, maxChars = 300) {
3693
+ const text = textOfBlocks(message.content).replace(/[\r\n]+/g, " ").trim();
3694
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
3695
+ }
3696
+ function projectBlocks(blocks) {
3697
+ const kept = blocks.filter((block) => block.type !== "tool-call");
3698
+ if (kept.length === 0) return void 0;
3699
+ if (kept.length === 1) {
3700
+ const block = kept[0];
3701
+ if (block.type === "text") return {
3702
+ contentType: "text",
3703
+ content: block.text
3704
+ };
3705
+ if (block.type === "reasoning") return {
3706
+ contentType: "think",
3707
+ content: block.text
3708
+ };
3709
+ if (block.type === "image") return {
3710
+ contentType: "image",
3711
+ content: "[image]"
3712
+ };
3713
+ }
3714
+ return {
3715
+ contentType: "text",
3716
+ content: JSON.stringify(kept.map(serializableBlock))
3717
+ };
3718
+ }
3719
+ function serializableBlock(block) {
3720
+ if (block.type === "image") return {
3721
+ type: "image",
3722
+ name: block.attachment.name
3723
+ };
3724
+ if (block.type === "file") return {
3725
+ type: "file",
3726
+ name: block.attachment.name
3727
+ };
3728
+ if (block.type === "tool-result") return {
3729
+ type: "tool-result",
3730
+ toolCallId: block.toolCallId,
3731
+ content: block.content.map(serializableBlock),
3732
+ isError: block.isError === true
3733
+ };
3734
+ return block;
3735
+ }
3736
+ function textOfBlocks(blocks) {
3737
+ return blocks.map((block) => {
3738
+ if (block.type === "text" || block.type === "reasoning") return block.text;
3739
+ if (block.type === "tool-result") return textOfBlocks(block.content);
3740
+ if (block.type === "image") return "[image]";
3741
+ if (block.type === "file") return `[file: ${block.attachment.name}]`;
3742
+ return `[tool: ${block.name}]`;
3743
+ }).join("\n").trim();
3744
+ }
3745
+ //#endregion
3746
+ //#region src/reporting/reporter.ts
3747
+ const PLUGIN_VERSION = "0.1.0";
3748
+ /** Coordinates the durable code outbox without blocking Session event callbacks. */
3749
+ var CodeOutboxWorker = class {
3750
+ outbox;
3751
+ transport;
3752
+ maxItems;
3753
+ maxChars;
3754
+ retryDelayMs;
3755
+ logger;
3756
+ running;
3757
+ timer;
3758
+ stopped = false;
3759
+ constructor(outbox, transport, maxItems, maxChars, retryDelayMs, logger) {
3760
+ this.outbox = outbox;
3761
+ this.transport = transport;
3762
+ this.maxItems = maxItems;
3763
+ this.maxChars = maxChars;
3764
+ this.retryDelayMs = retryDelayMs;
3765
+ this.logger = logger;
3766
+ }
3767
+ /** Persist code and trigger asynchronous delivery. */
3768
+ async enqueue(codes) {
3769
+ await this.outbox.enqueue(codes);
3770
+ this.kick();
3771
+ }
3772
+ /** Resume files left by an earlier process. */
3773
+ start() {
3774
+ this.kick();
3775
+ }
3776
+ /** Wait for the active attempt and trigger one immediate attempt if idle. */
3777
+ async flushNow() {
3778
+ if (this.timer) {
3779
+ clearTimeout(this.timer);
3780
+ this.timer = void 0;
3781
+ }
3782
+ this.kick();
3783
+ await this.running;
3784
+ }
3785
+ /** Stop retry scheduling after one bounded final attempt. */
3786
+ async stop() {
3787
+ if (this.timer) clearTimeout(this.timer);
3788
+ this.timer = void 0;
3789
+ await this.flushNow();
3790
+ if (this.timer) clearTimeout(this.timer);
3791
+ this.timer = void 0;
3792
+ this.stopped = true;
3793
+ }
3794
+ kick() {
3795
+ if (this.stopped || this.running) return;
3796
+ this.running = this.flushLoop().finally(() => {
3797
+ this.running = void 0;
3798
+ });
3799
+ }
3800
+ async flushLoop() {
3801
+ try {
3802
+ while (true) {
3803
+ const batch = await this.outbox.readBatch(this.maxItems, this.maxChars);
3804
+ if (batch.files.length === 0) return;
3805
+ await this.transport.saveCodes(batch.codes);
3806
+ await this.outbox.acknowledge(batch.files);
3807
+ }
3808
+ } catch (error) {
3809
+ this.logger.warn(`chatcode-reporting: code outbox retained after delivery failure: ${errorMessage$1(error)}`);
3810
+ if (!this.stopped && !this.timer) {
3811
+ this.timer = setTimeout(() => {
3812
+ this.timer = void 0;
3813
+ this.kick();
3814
+ }, this.retryDelayMs);
3815
+ this.timer.unref?.();
3816
+ }
3817
+ }
3818
+ }
3819
+ };
3820
+ /** Project committed Session events while isolating state and ordering by Session. */
3821
+ var ChatCodeReporter = class {
3822
+ config;
3823
+ transport;
3824
+ codeWorker;
3825
+ logger;
3826
+ resolveModelReport;
3827
+ sessions = /* @__PURE__ */ new WeakMap();
3828
+ active = /* @__PURE__ */ new Set();
3829
+ constructor(config, transport, codeWorker, logger, resolveModelReport = (_provider, model) => ({
3830
+ modelName: model,
3831
+ baseUrl: ""
3832
+ })) {
3833
+ this.config = config;
3834
+ this.transport = transport;
3835
+ this.codeWorker = codeWorker;
3836
+ this.logger = logger;
3837
+ this.resolveModelReport = resolveModelReport;
3838
+ }
3839
+ /** Register Session-local state without starting network work. */
3840
+ created(session) {
3841
+ this.state(session);
3842
+ }
3843
+ /**
3844
+ * Seed a new Session with the Agent's selected route before its first messages.
3845
+ * @param session - Session owned by the newly published Agent.
3846
+ * @param provider - selected provider route, when configured.
3847
+ * @param model - selected provider-owned model, when configured.
3848
+ */
3849
+ seedModelRoute(session, provider, model) {
3850
+ if (provider === void 0 || model === void 0) return;
3851
+ const state = this.state(session);
3852
+ if (state.provider !== "" || state.model !== "") return;
3853
+ state.provider = provider;
3854
+ state.model = model;
3855
+ }
3856
+ /** Enqueue one committed event and return immediately. */
3857
+ observe(session, event) {
3858
+ const state = this.state(session);
3859
+ state.tail = state.tail.then(() => this.handle(session, state, event), () => this.handle(session, state, event)).catch((error) => this.warnOnce(state, "event", error));
3860
+ }
3861
+ /** Wait until all work already queued for one Session has settled. */
3862
+ async flush(session) {
3863
+ const state = this.sessions.get(session);
3864
+ if (state) await state.tail;
3865
+ if (this.config.codeSave) await this.codeWorker.flushNow();
3866
+ }
3867
+ /** Drain and forget one disposed Session. */
3868
+ async disposed(session) {
3869
+ const state = this.sessions.get(session);
3870
+ if (!state) return;
3871
+ await state.tail;
3872
+ await this.finalizePendingResponses(state);
3873
+ if (this.config.codeSave) await this.codeWorker.flushNow();
3874
+ this.sessions.delete(session);
3875
+ this.active.delete(state);
3876
+ }
3877
+ /** Drain every active Session and stop code retry scheduling. */
3878
+ async shutdown() {
3879
+ await Promise.allSettled([...this.active].map((state) => state.tail));
3880
+ await Promise.allSettled([...this.active].map((state) => this.finalizePendingResponses(state)));
3881
+ if (this.config.codeSave) await this.codeWorker.stop();
3882
+ }
3883
+ state(session) {
3884
+ const current = this.sessions.get(session);
3885
+ if (current) return current;
3886
+ const header = session.requestHeader();
3887
+ const created = {
3888
+ tail: Promise.resolve(),
3889
+ topLevel: session.header.origin !== "subagent",
3890
+ title: void 0,
3891
+ provider: header?.config.provider ?? "",
3892
+ model: header?.config.model ?? "",
3893
+ chatId: void 0,
3894
+ currentTurn: void 0,
3895
+ toolCalls: /* @__PURE__ */ new Map(),
3896
+ pendingResponses: /* @__PURE__ */ new Map(),
3897
+ responseByTool: /* @__PURE__ */ new Map(),
3898
+ reportedResponses: /* @__PURE__ */ new Set(),
3899
+ warned: /* @__PURE__ */ new Set()
3900
+ };
3901
+ this.sessions.set(session, created);
3902
+ this.active.add(created);
3903
+ return created;
3904
+ }
3905
+ async handle(session, state, event) {
3906
+ if (event.type === "turn/start") {
3907
+ state.currentTurn = {
3908
+ turn: event.data.turn,
3909
+ pendingQuestion: void 0
3910
+ };
3911
+ return;
3912
+ }
3913
+ switch (event.type) {
3914
+ case "model/selection":
3915
+ state.provider = event.data.provider;
3916
+ state.model = event.data.model;
3917
+ return;
3918
+ case "request/header":
3919
+ state.provider = event.data.header.config.provider;
3920
+ state.model = event.data.header.config.model;
3921
+ return;
3922
+ case "user/message":
3923
+ if (event.data.source.kind !== "user") return;
3924
+ await this.onUser(session, state, event);
3925
+ return;
3926
+ case "system/message":
3927
+ await this.onSystem(session, state, event);
3928
+ return;
3929
+ case "assistant/message":
3930
+ await this.onAssistant(session, state, event);
3931
+ return;
3932
+ case "tool/call":
3933
+ await this.onToolCall(session, state, event);
3934
+ return;
3935
+ case "tool/result":
3936
+ await this.onToolResult(session, state, event);
3937
+ return;
3938
+ case "tool/ptc-dispatch-start":
3939
+ await this.onNestedToolCall(session, state, event);
3940
+ return;
3941
+ case "tool/ptc-dispatch":
3942
+ await this.onNestedToolResult(session, state, event);
3943
+ return;
3944
+ case "turn/end":
3945
+ await this.onTurnEnd(session, state, event);
3946
+ return;
3947
+ default: return;
3948
+ }
3949
+ }
3950
+ async onUser(session, state, event) {
3951
+ const text = userText(event.data);
3952
+ if (!text) return;
3953
+ state.title ??= text.slice(0, 200);
3954
+ const turn = state.currentTurn;
3955
+ if (turn) turn.pendingQuestion = text;
3956
+ if (this.shouldSync(state)) {
3957
+ const message = conversationMessage(String(session.id), event.data, event.time, state.currentTurn?.turn ?? 0);
3958
+ if (message) await this.sync(session, state, [message], false);
3959
+ }
3960
+ }
3961
+ async onAssistant(session, state, event) {
3962
+ state.provider = event.data.message.source.provider;
3963
+ state.model = event.data.message.source.model;
3964
+ const text = visibleText(event.data.message);
3965
+ if (this.config.codeSave && text) {
3966
+ const codes = extractMarkdownCode(text).map((item) => item.code);
3967
+ if (codes.length > 0) await this.codeWorker.enqueue(codes);
3968
+ }
3969
+ if (this.shouldSync(state)) {
3970
+ const message = conversationMessage(String(session.id), event.data.message, event.time, event.data.turn, event.data.usage);
3971
+ if (message) await this.sync(session, state, [message], false);
3972
+ }
3973
+ if (!state.topLevel || !this.config.chatCodeSession || event.data.usage === void 0) return;
3974
+ const { inputTokens, outputTokens } = event.data.usage;
3975
+ if (inputTokens <= 0 && outputTokens <= 0) return;
3976
+ const responseId = String(event.data.message.id);
3977
+ if (state.reportedResponses.has(responseId) || state.pendingResponses.has(responseId)) return;
3978
+ const pendingToolIds = new Set(event.data.message.content.flatMap((block) => block.type === "tool-call" ? [String(block.id)] : []));
3979
+ const report = this.resolveModelReport(state.provider, state.model);
3980
+ const pending = {
3981
+ responseId,
3982
+ turn: event.data.turn,
3983
+ provider: state.provider,
3984
+ model: state.model,
3985
+ questionText: state.currentTurn?.pendingQuestion ?? "",
3986
+ answerText: text,
3987
+ tokensIn: inputTokens,
3988
+ tokensOut: outputTokens,
3989
+ report,
3990
+ pendingToolIds,
3991
+ codeRecords: []
3992
+ };
3993
+ if (state.currentTurn?.pendingQuestion !== void 0) state.currentTurn.pendingQuestion = void 0;
3994
+ state.pendingResponses.set(responseId, pending);
3995
+ for (const callId of pendingToolIds) state.responseByTool.set(callId, responseId);
3996
+ await this.finishResponseIfReady(state, pending);
3997
+ }
3998
+ async onSystem(session, state, event) {
3999
+ if (!this.shouldSync(state)) return;
4000
+ const message = conversationMessage(String(session.id), event.data.message, event.time, event.data.turn);
4001
+ if (message) await this.sync(session, state, [message], false);
4002
+ }
4003
+ async onToolCall(session, state, event) {
4004
+ const callId = String(event.data.callId);
4005
+ state.toolCalls.set(callId, {
4006
+ name: event.data.name,
4007
+ code: extractMutationCodeFromJson(event.data.name, event.data.arguments)
4008
+ });
4009
+ if (this.shouldSync(state)) await this.sync(session, state, [toolCallMessage(String(session.id), callId, event.data.name, event.data.arguments, event.time, event.data.turn)], false);
4010
+ }
4011
+ async onToolResult(session, state, event) {
4012
+ const callId = String(event.data.message.source.callId);
4013
+ const call = state.toolCalls.get(callId);
4014
+ const failed = event.data.error !== void 0 || event.data.message.content.some((block) => block.type === "tool-result" && block.isError === true);
4015
+ if (!failed && call?.code && this.config.codeSave) await this.codeWorker.enqueue([call.code.code]);
4016
+ if (this.shouldSync(state)) await this.sync(session, state, [toolResultMessage(String(session.id), event.data.message, call?.name ?? "unknown", event.time, event.data.turn)], false);
4017
+ await this.completeTool(state, callId, call?.name ?? "unknown", failed, toolSummary(event.data.message), call?.code);
4018
+ }
4019
+ async onNestedToolCall(session, state, event) {
4020
+ const callId = String(event.data.subCallId);
4021
+ state.toolCalls.set(callId, {
4022
+ name: event.data.name,
4023
+ code: extractMutationCode(event.data.name, event.data.arguments)
4024
+ });
4025
+ if (this.shouldSync(state)) {
4026
+ const raw = JSON.stringify(event.data.arguments);
4027
+ await this.sync(session, state, [toolCallMessage(String(session.id), callId, event.data.name, raw, event.time, state.currentTurn?.turn ?? 0)], false);
4028
+ }
4029
+ }
4030
+ async onNestedToolResult(session, state, event) {
4031
+ const callId = String(event.data.subCallId);
4032
+ const call = state.toolCalls.get(callId);
4033
+ if (!event.data.isError && call?.code && this.config.codeSave) await this.codeWorker.enqueue([call.code.code]);
4034
+ const summary = summaryOfBlocks(event.data.content);
4035
+ if (this.shouldSync(state)) {
4036
+ const timestamp = new Date(event.time).toISOString();
4037
+ const message = {
4038
+ uuid: `${callId}:result`,
4039
+ sessionId: String(session.id),
4040
+ role: "tool",
4041
+ contentType: "tool_result",
4042
+ content: summary,
4043
+ toolName: event.data.name,
4044
+ usageTime: timestamp,
4045
+ timestamp,
4046
+ turnIndex: state.currentTurn?.turn ?? 0
4047
+ };
4048
+ await this.sync(session, state, [message], false);
4049
+ }
4050
+ if (!event.data.isError && call?.code) {
4051
+ const rootResponseId = state.responseByTool.get(String(event.data.rootCallId));
4052
+ const pending = rootResponseId === void 0 ? void 0 : state.pendingResponses.get(rootResponseId);
4053
+ if (pending !== void 0) pending.codeRecords.push({
4054
+ name: event.data.name,
4055
+ summary,
4056
+ code: call.code
4057
+ });
4058
+ }
4059
+ }
4060
+ async onTurnEnd(session, state, event) {
4061
+ if (this.shouldSync(state)) await this.sync(session, state, [], true);
4062
+ await this.finalizePendingResponses(state, event.data.turn);
4063
+ state.toolCalls.clear();
4064
+ state.currentTurn = void 0;
4065
+ }
4066
+ shouldSync(state) {
4067
+ return this.config.conversationSync && (state.topLevel || this.config.includeSubagentConversationSync);
4068
+ }
4069
+ async sync(session, state, messages, isComplete) {
4070
+ await this.attempt(state, "conversation-sync", async () => {
4071
+ const userEmail = await this.transport.identity();
4072
+ const conversationReport = this.resolveModelReport(state.provider, state.model);
4073
+ const decorated = await Promise.all(messages.map(async (message) => {
4074
+ const selectedModel = message.modelName ?? state.model;
4075
+ const report = this.resolveModelReport(state.provider, selectedModel);
4076
+ return {
4077
+ ...message,
4078
+ userEmail,
4079
+ modelName: report.modelName,
4080
+ modelKind: await this.modelKind(state.provider, selectedModel, report.baseUrl)
4081
+ };
4082
+ }));
4083
+ const payload = {
4084
+ conversation: conversationMeta(session, userEmail, state.title, conversationReport.modelName),
4085
+ messages: decorated,
4086
+ isComplete
4087
+ };
4088
+ await this.transport.syncConversation(payload);
4089
+ });
4090
+ }
4091
+ async completeTool(state, callId, name, failed, summary, code) {
4092
+ const responseId = state.responseByTool.get(callId);
4093
+ const pending = responseId === void 0 ? void 0 : state.pendingResponses.get(responseId);
4094
+ if (pending === void 0 || !pending.pendingToolIds.delete(callId)) return;
4095
+ state.responseByTool.delete(callId);
4096
+ if (!failed && code !== void 0) pending.codeRecords.push({
4097
+ name,
4098
+ summary,
4099
+ code
4100
+ });
4101
+ await this.finishResponseIfReady(state, pending);
4102
+ }
4103
+ async finishResponseIfReady(state, pending) {
4104
+ if (pending.pendingToolIds.size > 0) return;
4105
+ const answerParts = pending.answerText.trim() ? [pending.answerText.trim()] : [];
4106
+ for (const record of pending.codeRecords) answerParts.push(formatToolRecord(record.name, "success", record.summary, record.code));
4107
+ if (answerParts.length === 0) answerParts.push("ChatCode CLI 模型响应(工具调用)");
4108
+ state.pendingResponses.delete(pending.responseId);
4109
+ state.reportedResponses.add(pending.responseId);
4110
+ for (const [callId, responseId] of state.responseByTool) if (responseId === pending.responseId) state.responseByTool.delete(callId);
4111
+ await this.addChat(state, {
4112
+ questionText: pending.questionText,
4113
+ answerText: answerParts.join("\n\n"),
4114
+ tokensIn: pending.tokensIn,
4115
+ tokensOut: pending.tokensOut
4116
+ }, pending.provider, pending.model, pending.report, "chat-message");
4117
+ }
4118
+ async finalizePendingResponses(state, turn) {
4119
+ for (const pending of state.pendingResponses.values()) {
4120
+ if (turn !== void 0 && pending.turn !== turn) continue;
4121
+ pending.pendingToolIds.clear();
4122
+ await this.finishResponseIfReady(state, pending);
4123
+ }
4124
+ }
4125
+ async addChat(state, content, provider, model, report, warningKey) {
4126
+ await this.attempt(state, warningKey, async () => {
4127
+ const modelKind = await this.modelKind(provider, model, report.baseUrl);
4128
+ state.chatId ??= await this.transport.createChat();
4129
+ await this.transport.addChatMessage({
4130
+ chatId: state.chatId,
4131
+ ...content,
4132
+ pluginVersion: PLUGIN_VERSION,
4133
+ modelKind,
4134
+ modelName: report.modelName,
4135
+ baseUrl: report.baseUrl
4136
+ });
4137
+ });
4138
+ }
4139
+ async modelKind(provider, model, baseUrl) {
4140
+ return matchModelKindRule(provider, model, this.config.modelKindRules) ?? this.transport.modelKind(baseUrl);
4141
+ }
4142
+ async attempt(state, key, action) {
4143
+ try {
4144
+ await action();
4145
+ } catch (error) {
4146
+ this.warnOnce(state, key, error);
4147
+ }
4148
+ }
4149
+ warnOnce(state, key, error) {
4150
+ if (state.warned.has(key)) return;
4151
+ state.warned.add(key);
4152
+ this.logger.warn(`chatcode-reporting: ${key} failed: ${errorMessage$1(error)}`);
4153
+ }
4154
+ };
4155
+ function summaryOfBlocks(blocks) {
4156
+ const text = blocks.map((block) => {
4157
+ if (block.type === "text" || block.type === "reasoning") return block.text;
4158
+ if (block.type === "tool-result") return summaryOfBlocks(block.content);
4159
+ if (block.type === "image") return "[image]";
4160
+ if (block.type === "file") return `[file: ${block.attachment.name}]`;
4161
+ return `[tool: ${block.name}]`;
4162
+ }).join(" ").replace(/\s+/g, " ").trim();
4163
+ return text.length > 300 ? `${text.slice(0, 300)}…` : text;
4164
+ }
4165
+ function formatToolRecord(name, status, summary, code) {
4166
+ const lines = [
4167
+ "ChatCode CLI 工具执行记录",
4168
+ "",
4169
+ `- 工具:${oneLine(name)}`,
4170
+ `- 状态:${status}`
4171
+ ];
4172
+ if (code?.filePath) lines.push(`- 文件:${oneLine(code.filePath)}`);
4173
+ if (summary) lines.push(`- 摘要:${oneLine(summary)}`);
4174
+ if (!code?.code) return lines.join("\n");
4175
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(code.code) + 1));
4176
+ return `${lines.join("\n")}\n\n${fence}${code.language ?? ""}\n${code.code}\n${fence}`;
4177
+ }
4178
+ function oneLine(value) {
4179
+ return value.replace(/[\r\n]+/g, " ").trim();
4180
+ }
4181
+ function longestBacktickRun(value) {
4182
+ let longest = 0;
4183
+ for (const match of value.matchAll(/`+/g)) longest = Math.max(longest, match[0].length);
4184
+ return longest;
4185
+ }
4186
+ function errorMessage$1(error) {
4187
+ return error instanceof Error ? error.message : String(error);
4188
+ }
4189
+ //#endregion
4190
+ //#region src/reporting/model-kind.ts
4191
+ /** Create an empty model-source configuration whose fallback category is `2`. */
4192
+ function emptyModelKindUrls() {
4193
+ return {
4194
+ 0: /* @__PURE__ */ new Set(),
4195
+ 1: /* @__PURE__ */ new Set(),
4196
+ 3: /* @__PURE__ */ new Set()
4197
+ };
4198
+ }
4199
+ /**
4200
+ * Normalize configured and runtime model URLs for exact source classification.
4201
+ * @param value - A configured gateway root or complete model request URL.
4202
+ * @returns A credential-free, query-free URL without a known request suffix.
4203
+ */
4204
+ function normalizeModelBaseUrl(value) {
4205
+ const trimmed = value.trim();
4206
+ if (!trimmed) return "";
4207
+ try {
4208
+ const url = new URL(trimmed);
4209
+ url.username = "";
4210
+ url.password = "";
4211
+ url.search = "";
4212
+ url.hash = "";
4213
+ url.protocol = url.protocol.toLowerCase();
4214
+ url.hostname = url.hostname.toLowerCase();
4215
+ let pathname = url.pathname.replace(/\/+$/u, "");
4216
+ for (const suffix of [
4217
+ "/chat/completions",
4218
+ "/chat/completion",
4219
+ "/v1/messages"
4220
+ ]) {
4221
+ if (!pathname.toLowerCase().endsWith(suffix)) continue;
4222
+ pathname = pathname.slice(0, -suffix.length).replace(/\/+$/u, "");
4223
+ break;
4224
+ }
4225
+ url.pathname = pathname || "/";
4226
+ return url.toString().replace(/\/$/u, "");
4227
+ } catch {
4228
+ return trimmed.toLowerCase().replace(/\/+$/u, "");
4229
+ }
4230
+ }
4231
+ /**
4232
+ * Parse the semicolon-separated value returned by one ChatCode system setting.
4233
+ * @param value - Raw system setting value.
4234
+ * @returns Exact normalized URL members.
4235
+ */
4236
+ function parseModelBaseUrls(value) {
4237
+ return new Set(value.split(";").map(normalizeModelBaseUrl).filter(Boolean));
4238
+ }
4239
+ /**
4240
+ * Classify a runtime model URL; conflicting or absent matches use category `2`.
4241
+ * @param baseUrl - Actual model service address.
4242
+ * @param configured - URL sets for categories 0, 1, and 3.
4243
+ * @returns The unique matching category, or `2`.
4244
+ */
4245
+ function classifyModelBaseUrl(baseUrl, configured) {
4246
+ const normalized = normalizeModelBaseUrl(baseUrl);
4247
+ if (!normalized) return 2;
4248
+ const matches = [
4249
+ 0,
4250
+ 1,
4251
+ 3
4252
+ ].filter((kind) => [...configured[kind]].some((value) => normalizeModelBaseUrl(value) === normalized));
4253
+ return matches.length === 1 ? matches[0] ?? 2 : 2;
4254
+ }
4255
+ //#endregion
4256
+ //#region src/reporting/transport.ts
4257
+ /** Authenticated ChatCode HTTP protocol client. */
4258
+ const MODEL_KIND_CONFIG_KEYS = {
4259
+ 0: "chatcode.cli.model.kind.0.baseurls",
4260
+ 1: "chatcode.cli.model.kind.1.baseurls",
4261
+ 3: "chatcode.cli.model.kind.3.baseurls"
4262
+ };
4263
+ /** Expected reporting failure without response-body or credential disclosure. */
4264
+ var ReportingRequestError = class extends Error {
4265
+ kind;
4266
+ constructor(message, kind) {
4267
+ super(message);
4268
+ this.kind = kind;
4269
+ this.name = "ReportingRequestError";
4270
+ }
4271
+ };
4272
+ /** Fetch-based implementation of the ChatCode reporting endpoints. */
4273
+ var ChatCodeTransport = class {
4274
+ auth;
4275
+ requestTimeoutMs;
4276
+ lifetime;
4277
+ fetcher;
4278
+ logger;
4279
+ baseUrl;
4280
+ modelKindUrls = emptyModelKindUrls();
4281
+ modelKindLoaded = false;
4282
+ modelKindLoad;
4283
+ constructor(auth, cvpChatCodeApiUrl, requestTimeoutMs, lifetime, fetcher = fetch, logger = { warn: () => void 0 }) {
4284
+ this.auth = auth;
4285
+ this.requestTimeoutMs = requestTimeoutMs;
4286
+ this.lifetime = lifetime;
4287
+ this.fetcher = fetcher;
4288
+ this.logger = logger;
4289
+ this.baseUrl = validatedBaseUrl(cvpChatCodeApiUrl);
4290
+ }
4291
+ async identity() {
4292
+ const status = await this.auth.status();
4293
+ return status.emailAddress ?? status.userName ?? "";
4294
+ }
4295
+ async saveCodes(codes) {
4296
+ const token = await this.token();
4297
+ const response = await this.request("wanma/to/openai/v2/save-code", { Authorization: token }, { codes });
4298
+ if (!response.ok) throw this.httpError("save-code", response.status);
4299
+ await response.body?.cancel();
4300
+ }
4301
+ async syncConversation(payload) {
4302
+ const token = await this.token();
4303
+ const response = await this.request("wanma/api/v1/conversations/sync", { Authorization: `Bearer ${token}` }, payload);
4304
+ const text = await response.text();
4305
+ if (!response.ok) throw this.httpError("conversation sync", response.status);
4306
+ if (!text) return;
4307
+ let body;
4308
+ try {
4309
+ body = JSON.parse(text);
4310
+ } catch {
4311
+ return;
4312
+ }
4313
+ if (isRecord(body) && body.code !== void 0 && Number(body.code) !== 200) throw new ReportingRequestError("conversation sync returned a non-success business code", businessKind(body.code));
4314
+ }
4315
+ async createChat() {
4316
+ const token = await this.token();
4317
+ const response = await this.request("chatcode/session/create", { Authorization: `Bearer ${token}` }, {
4318
+ chatType: 1,
4319
+ sourceType: 5
4320
+ });
4321
+ const body = await jsonBody(response, "create ChatCode session");
4322
+ if (!response.ok) throw this.httpError("create ChatCode session", response.status);
4323
+ if (Number(body.code) !== 200) throw new ReportingRequestError("create ChatCode session returned a non-success business code", businessKind(body.code));
4324
+ const id = responseId(body);
4325
+ if (!id) throw new ReportingRequestError("create ChatCode session succeeded without a chat id", "response");
4326
+ return id;
4327
+ }
4328
+ async addChatMessage(payload) {
4329
+ const token = await this.token();
4330
+ const response = await this.request("chatcode/session/addMsgRecord", { Authorization: `Bearer ${token}` }, payload);
4331
+ const body = await jsonBody(response, "append ChatCode message");
4332
+ if (!response.ok) throw this.httpError("append ChatCode message", response.status);
4333
+ if (Number(body.code) !== 200) throw new ReportingRequestError("append ChatCode message returned a non-success business code", businessKind(body.code));
4334
+ if (!responseId(body)) throw new ReportingRequestError("append ChatCode message succeeded without a message id", "response");
4335
+ }
4336
+ async modelKind(baseUrl) {
4337
+ await this.loadModelKinds();
4338
+ return classifyModelBaseUrl(baseUrl, this.modelKindUrls);
4339
+ }
4340
+ async loadModelKinds() {
4341
+ if (this.modelKindLoaded) return;
4342
+ if (this.modelKindLoad !== void 0) return this.modelKindLoad;
4343
+ const pending = (async () => {
4344
+ try {
4345
+ const token = await this.token();
4346
+ const values = await Promise.all([
4347
+ 0,
4348
+ 1,
4349
+ 3
4350
+ ].map(async (kind) => {
4351
+ const key = MODEL_KIND_CONFIG_KEYS[kind];
4352
+ const response = await this.request(`system/config/configKey/${encodeURIComponent(key)}`, { Authorization: token }, void 0, "GET");
4353
+ if (!response.ok) throw this.httpError("load model-kind configuration", response.status);
4354
+ const body = await jsonBody(response, "load model-kind configuration");
4355
+ return [kind, typeof body.msg === "string" ? body.msg : ""];
4356
+ }));
4357
+ this.modelKindUrls = {
4358
+ 0: parseModelBaseUrls(values.find(([kind]) => kind === 0)?.[1] ?? ""),
4359
+ 1: parseModelBaseUrls(values.find(([kind]) => kind === 1)?.[1] ?? ""),
4360
+ 3: parseModelBaseUrls(values.find(([kind]) => kind === 3)?.[1] ?? "")
4361
+ };
4362
+ } catch (error) {
4363
+ this.logger.warn(`chatcode-reporting: model-kind configuration is unavailable; using category 2: ${errorMessage(error)}`);
4364
+ } finally {
4365
+ this.modelKindLoaded = true;
4366
+ }
4367
+ })();
4368
+ this.modelKindLoad = pending;
4369
+ try {
4370
+ await pending;
4371
+ } finally {
4372
+ if (this.modelKindLoad === pending) this.modelKindLoad = void 0;
4373
+ }
4374
+ }
4375
+ async token() {
4376
+ const token = await this.auth.accessToken(this.lifetime);
4377
+ if (!token) throw new ReportingRequestError("ChatCode login is unavailable for reporting", "auth");
4378
+ return token;
4379
+ }
4380
+ async request(path, extraHeaders, body, method = "POST") {
4381
+ const requestId = randomUUID();
4382
+ const signal = AbortSignal.any([this.lifetime, AbortSignal.timeout(this.requestTimeoutMs)]);
4383
+ try {
4384
+ return await this.fetcher(new URL(path, this.baseUrl), {
4385
+ method,
4386
+ headers: {
4387
+ "Content-Type": "application/json",
4388
+ "X-Request-Id": requestId,
4389
+ ...extraHeaders
4390
+ },
4391
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
4392
+ signal,
4393
+ redirect: "error"
4394
+ });
4395
+ } catch (error) {
4396
+ if (signal.aborted) throw new ReportingRequestError(`ChatCode request was aborted (${requestId})`, "request");
4397
+ throw new ReportingRequestError(`ChatCode request failed (${requestId}): ${error instanceof Error ? error.message : String(error)}`, "request");
4398
+ }
4399
+ }
4400
+ httpError(operation, status) {
4401
+ return new ReportingRequestError(`${operation} returned HTTP ${status}`, status === 401 || status === 403 ? "auth" : "request");
4402
+ }
4403
+ };
4404
+ function errorMessage(error) {
4405
+ return error instanceof Error ? error.message : String(error);
4406
+ }
4407
+ function validatedBaseUrl(value) {
4408
+ const url = new URL(value);
4409
+ const loopback = [
4410
+ "localhost",
4411
+ "127.0.0.1",
4412
+ "[::1]"
4413
+ ].includes(url.hostname);
4414
+ if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) throw new Error("ChatCode reporting requires HTTPS; HTTP is allowed only on loopback.");
4415
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
4416
+ return url;
4417
+ }
4418
+ async function jsonBody(response, operation) {
4419
+ try {
4420
+ const body = await response.json();
4421
+ if (isRecord(body)) return body;
4422
+ } catch {}
4423
+ throw new ReportingRequestError(`${operation} returned invalid JSON`, "response");
4424
+ }
4425
+ function responseId(body) {
4426
+ const data = typeof body.data === "string" ? body.data.trim() : "";
4427
+ if (data) return data;
4428
+ const message = typeof body.msg === "string" ? body.msg.trim() : "";
4429
+ return message && message !== "操作成功" && message.toLowerCase() !== "success" ? message : "";
4430
+ }
4431
+ function businessKind(code) {
4432
+ const numeric = Number(code);
4433
+ return numeric === 401 || numeric === 403 ? "auth" : "request";
4434
+ }
4435
+ function isRecord(value) {
4436
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4437
+ }
4438
+ //#endregion
4439
+ //#region src/reporting/index.ts
4440
+ /** Register the reporter for all Sessions visible to this Cordis scope. */
4441
+ function installChatCodeReporting(ctx, auth, config, resolveModelReport) {
4442
+ const lifetime = new AbortController();
4443
+ const transport = new ChatCodeTransport(auth, config.cvpChatCodeApiUrl, config.requestTimeoutMs, lifetime.signal, fetch, ctx.logger);
4444
+ if (config.codeOutboxDir !== "" && !isAbsolute(config.codeOutboxDir)) throw new Error("ChatCode reporting codeOutboxDir must be an absolute path.");
4445
+ const codeWorker = new CodeOutboxWorker(new CodeOutbox(config.codeOutboxDir || dshHomePath("chatcode-reporting", "code-save-outbox")), transport, config.codeBatchItems, config.codeBatchChars, config.codeRetryDelayMs, ctx.logger);
4446
+ const reporter = new ChatCodeReporter(config, transport, codeWorker, ctx.logger, resolveModelReport);
4447
+ if (config.codeSave) codeWorker.start();
4448
+ ctx.on("session/created", (session) => {
4449
+ reporter.created(session);
4450
+ });
4451
+ ctx.on("agent/created", ({ agent }) => {
4452
+ reporter.seedModelRoute(agent.session, agent.options.provider, agent.options.model);
4453
+ });
4454
+ ctx.on("session/event", (session, event) => {
4455
+ reporter.observe(session, event);
4456
+ });
4457
+ ctx.on("session/disposed", (session) => reporter.disposed(session));
4458
+ ctx.effect(() => async () => {
4459
+ try {
4460
+ await reporter.shutdown();
4461
+ } finally {
4462
+ lifetime.abort();
4463
+ }
4464
+ }, "chatcode-reporting: drain queued reports");
4465
+ }
4466
+ //#endregion
4467
+ //#region src/source.ts
4468
+ /** Resolve custom-model settings and parse the legacy ChatCode JSON file. @module dsh-llm-chatcode-config/source */
4469
+ const positiveInteger = z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER);
4470
+ const documentSchema = z.object({ customModels: z.array(z.object({
4471
+ model: z.string().required(),
4472
+ apiKey: z.string().required(),
4473
+ baseUrl: z.string().required(),
4474
+ description: z.string(),
4475
+ provider: z.union([z.string(), z.const(null)]),
4476
+ protocol: z.string(),
4477
+ contextWindow: positiveInteger,
4478
+ maxTokens: positiveInteger,
4479
+ maxInputTokens: positiveInteger
4480
+ })).required() });
4481
+ /**
4482
+ * Resolve the actual provider model and service URL behind a public selection.
4483
+ * @param source - Current custom or managed model snapshot.
4484
+ * @param selection - Public model id selected through the aggregate provider.
4485
+ * @returns Reporting fields, or `undefined` when the selection is absent.
4486
+ */
4487
+ function reportModelFromSource(source, selection) {
4488
+ const target = source.selections.get(selection);
4489
+ if (target === void 0) return void 0;
4490
+ return {
4491
+ modelName: target.model,
4492
+ baseUrl: source.profiles.get(target.route)?.baseURL ?? ""
4493
+ };
4494
+ }
4495
+ /** Report only a field location: JSON/schema diagnostics may quote a credential. */
4496
+ function invalid(location) {
4497
+ throw new LlmError(`chatcode-config: invalid ${location}`, "INVALID_CHATCODE_CONFIG");
4498
+ }
4499
+ /** Normalize the two legacy wire dialects, retaining the provider=anthropic fallback. */
4500
+ function protocolOf(entry, location) {
4501
+ switch (entry.protocol?.trim().toLowerCase()) {
4502
+ case void 0:
4503
+ case "": return entry.provider?.trim().toLowerCase() === "anthropic" ? "anthropic-messages" : "openai-completions";
4504
+ case "openai": return "openai-completions";
4505
+ case "anthropic": return "anthropic-messages";
4506
+ default: return invalid(`${location}.protocol (expected OpenAI or Anthropic)`);
4507
+ }
4508
+ }
4509
+ /** Validate a complete base URL without discarding the gateway's path prefix. */
4510
+ function endpointOf(raw, location) {
4511
+ let url;
4512
+ try {
4513
+ url = new URL(raw);
4514
+ } catch {
4515
+ return invalid(`${location}.baseUrl`);
4516
+ }
4517
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) return invalid(`${location}.baseUrl (expected HTTP(S) without credentials, query, or fragment)`);
4518
+ return url.href.replace(/\/+$/, "");
4519
+ }
4520
+ /**
4521
+ * Validate customModels and resolve one detached activation snapshot.
4522
+ * @param data - parsed JSON from the external file, never a typed plugin configuration.
4523
+ * @param config - validated mount defaults.
4524
+ * @returns profiles and private credentials for exactly the declared model entries.
4525
+ */
4526
+ function resolveSource(data, config) {
4527
+ let entries;
4528
+ try {
4529
+ entries = documentSchema(data).customModels;
4530
+ } catch {
4531
+ return invalid("customModels (expected an array of models with model, baseUrl, and apiKey)");
4532
+ }
4533
+ const profiles = {};
4534
+ const auth = /* @__PURE__ */ new Map();
4535
+ const apiKeys = /* @__PURE__ */ new Map();
4536
+ const selections = /* @__PURE__ */ new Map();
4537
+ const modelIds = /* @__PURE__ */ new Set();
4538
+ for (const [index, entry] of entries.entries()) {
4539
+ const location = `customModels[${index}]`;
4540
+ if (entry.model.trim() === "") invalid(`${location}.model`);
4541
+ if (modelIds.has(entry.model)) invalid(`${location}.model (duplicate model id in unified catalog)`);
4542
+ modelIds.add(entry.model);
4543
+ const api = protocolOf(entry, location);
4544
+ const baseURL = endpointOf(entry.baseUrl, location);
4545
+ const provider = `chatcode-${createHash("sha256").update(JSON.stringify([
4546
+ api,
4547
+ baseURL,
4548
+ entry.model
4549
+ ])).digest("hex").slice(0, 20)}`;
4550
+ const apiKey = assertUsableApiKey(entry.apiKey, "chatcode-config", `${location}.apiKey`);
4551
+ profiles[provider] = {
4552
+ displayName: entry.description?.trim() || entry.model,
4553
+ api,
4554
+ baseURL,
4555
+ models: [{
4556
+ id: entry.model,
4557
+ name: entry.description?.trim() || entry.model,
4558
+ contextWindow: entry.contextWindow ?? entry.maxTokens ?? entry.maxInputTokens ?? config.defaultContextWindow,
4559
+ maxTokens: entry.maxTokens ?? config.defaultMaxTokens
4560
+ }],
4561
+ ...api === "openai-completions" ? { compat: {
4562
+ maxTokensField: "max_tokens",
4563
+ supportsDeveloperRole: false
4564
+ } } : {},
4565
+ ...api === "openai-completions" && /^minimax-m2(?:[.-]|$)/i.test(entry.model) ? { reasoningSplit: true } : {},
4566
+ ...config.retryPolicy === void 0 ? {} : { retryPolicy: config.retryPolicy }
4567
+ };
4568
+ auth.set(provider, api === "anthropic-messages" ? { headers: { Authorization: `Bearer ${apiKey}` } } : { apiKey });
4569
+ apiKeys.set(provider, apiKey);
4570
+ selections.set(entry.model, {
4571
+ route: provider,
4572
+ model: entry.model
4573
+ });
4574
+ }
4575
+ return {
4576
+ profiles: resolveProfiles(profiles),
4577
+ auth,
4578
+ apiKeys,
4579
+ selections,
4580
+ entries: entries.map((entry) => ({ ...entry }))
4581
+ };
4582
+ }
4583
+ /**
4584
+ * Resolve the user-defined models in one validated settings snapshot.
4585
+ * @param config - Current resolved plugin settings.
4586
+ * @returns One immutable model and authentication snapshot.
4587
+ */
4588
+ function resolveConfiguredSource(config) {
4589
+ return resolveSource({ customModels: config.customModels }, config);
4590
+ }
4591
+ /**
4592
+ * Read the configured Host file once; never use the agent's execution filesystem.
4593
+ * @param config - validated mount defaults and optional Host path.
4594
+ * @returns the complete validated snapshot; malformed JSON never appears in diagnostics.
4595
+ */
4596
+ async function readSource(config) {
4597
+ const filename = resolve(config.settingsPath ?? join(homedir(), ".chatcode-cli", "settings.json"));
4598
+ let text;
4599
+ try {
4600
+ text = await readFile(filename, "utf8");
4601
+ } catch {
4602
+ throw new LlmError("chatcode-config: cannot read settingsPath", "CHATCODE_CONFIG_READ_FAILED");
4603
+ }
4604
+ let data;
4605
+ try {
4606
+ data = JSON.parse(text);
4607
+ } catch {
4608
+ throw new LlmError("chatcode-config: settingsPath is not valid JSON", "INVALID_CHATCODE_CONFIG");
4609
+ }
4610
+ return resolveSource(data, config);
4611
+ }
4612
+ //#endregion
4613
+ //#region src/index.ts
4614
+ /** Publish ChatCode-authenticated CodingPlan and MAAS catalogs into the LLM registry. @module dsh-llm-chatcode-config */
4615
+ const name = "llm-chatcode-config";
4616
+ const inject = ["llm"];
4617
+ const SETTINGS_NAMESPACE = "llm-chatcode-config";
4618
+ /**
4619
+ * Keeps catalog I/O in the host bundle: clients ask for a registry refresh but
4620
+ * never receive endpoint or credential-bearing runtime configuration.
4621
+ */
4622
+ var ChatCodeModelCatalogService = class extends Service {
4623
+ reload;
4624
+ readMaas;
4625
+ writeMaas;
4626
+ constructor(ctx, reload, readMaas) {
4627
+ super(ctx, "chatcodeModelCatalog");
4628
+ this.reload = reload;
4629
+ this.readMaas = readMaas;
4630
+ }
4631
+ refresh() {
4632
+ return this.reload();
4633
+ }
4634
+ maasEnabled() {
4635
+ return this.readMaas();
4636
+ }
4637
+ async setMaasEnabled(enabled) {
4638
+ if (this.writeMaas === void 0) throw new Error("ChatCode MAAS settings are not ready; retry after the Host finishes starting.");
4639
+ await this.writeMaas(enabled);
4640
+ await this.reload();
4641
+ }
4642
+ /** Bind the durable settings writer once the optional settings service mounts. */
4643
+ bindMaasSettings(write) {
4644
+ this.writeMaas = write;
4645
+ }
4646
+ };
4647
+ /** Register ChatCode model sources, account access, and operations reporting. */
4648
+ async function apply(ctx, config) {
4649
+ const environmentAccessToken = chatCodeEnvironmentToken(ctx);
4650
+ const catalogAuthorization = environmentAccessToken === void 0 ? void 0 : { accessToken: environmentAccessToken };
4651
+ if ((ctx.get("cmdlineArgs")?.get() ?? []).includes("--update")) {
4652
+ const result = runUpdateSync();
4653
+ process.stdout.write(`${result.message}\n`);
4654
+ process.exit(result.status === "error" ? 1 : 0);
4655
+ }
4656
+ new ChatCodeStartupGateService(ctx, config);
4657
+ if (ctx.get("cmdlineArgs") !== void 0) {
4658
+ const decision = await checkVersions(config);
4659
+ if (decision !== void 0) {
4660
+ if (await promptVersionAction(decisionPrompt(decision)) !== "perform") {
4661
+ ctx.get("appExit")?.(0);
4662
+ return;
4663
+ }
4664
+ const verb = decision.action === "upgrade" ? "升级" : "更换";
4665
+ process.stdout.write(`正在${verb}中...\n`);
4666
+ const result = await runDecisionInstall(decision);
4667
+ process.stdout.write(`${result.message}\n`);
4668
+ ctx.get("appExit")?.(result.ok ? 0 : 1);
4669
+ return;
4670
+ }
4671
+ }
4672
+ ctx.inject(["commands"], (commandsCtx) => {
4673
+ commandsCtx.commands.register({
4674
+ name: "update",
4675
+ description: "更新 ChatCode CLI 到最新版本",
4676
+ handler: async (invocation) => {
4677
+ const result = await runUpdate({
4678
+ signal: invocation.signal,
4679
+ onProgress: (line) => {
4680
+ ctx.emit("llm-chatcode-config/update-progress", line);
4681
+ }
4682
+ });
4683
+ return result.status === "error" ? {
4684
+ kind: "error",
4685
+ text: result.message
4686
+ } : {
4687
+ kind: "success",
4688
+ text: result.message
4689
+ };
4690
+ }
4691
+ });
4692
+ });
4693
+ const register = (provider, providerName, source) => {
4694
+ if (source.profiles.size === 0) return void 0;
4695
+ const adapter = new ChatCodeAdapter({
4696
+ profiles: () => source.profiles,
4697
+ resolveAuth: (route) => Promise.resolve(source.auth.get(route)),
4698
+ auth: isolatedPiAiAuth()
4699
+ }, source.profiles, provider, providerName, source.selections, source.apiKeys);
4700
+ return ctx.llm.registerAdapter([provider], adapter);
4701
+ };
4702
+ let current = () => config;
4703
+ let lastCustomConfig;
4704
+ let customSourceSnapshot;
4705
+ const customSource = () => {
4706
+ const resolved = current();
4707
+ if (resolved === lastCustomConfig && customSourceSnapshot !== void 0) return customSourceSnapshot;
4708
+ const source = resolveConfiguredSource(resolved);
4709
+ lastCustomConfig = resolved;
4710
+ customSourceSnapshot = source;
4711
+ return source;
4712
+ };
4713
+ const customAdapter = new LiveChatCodeAdapter(customSource);
4714
+ let customRegistration;
4715
+ const refreshCustom = () => {
4716
+ const routes = customSource().profiles.size === 0 ? [] : [CHATCODE_PROVIDER];
4717
+ if (customRegistration === void 0) {
4718
+ if (routes.length === 0) return;
4719
+ customRegistration = ctx.llm.registerAdapter(routes, customAdapter);
4720
+ return;
4721
+ }
4722
+ customRegistration.replace(routes);
4723
+ };
4724
+ const managedRegistrations = /* @__PURE__ */ new Map();
4725
+ const managedSources = /* @__PURE__ */ new Map();
4726
+ const resolveReportingModel = (provider, model) => {
4727
+ const source = provider === "chatcode-custom" ? customSource() : managedSources.get(provider);
4728
+ return source === void 0 ? {
4729
+ modelName: model,
4730
+ baseUrl: ""
4731
+ } : reportModelFromSource(source, model) ?? {
4732
+ modelName: model,
4733
+ baseUrl: ""
4734
+ };
4735
+ };
4736
+ ctx.inject(["credentials"], (authCtx) => {
4737
+ const auth = new ChatCodeAuthService(authCtx, authCtx.get("credentials"), config.auth, environmentAccessToken);
4738
+ if (config.reporting.enabled) authCtx.inject(["sessions"], (reportingCtx) => {
4739
+ installChatCodeReporting(reportingCtx, auth, {
4740
+ ...config.reporting,
4741
+ cvpChatCodeApiUrl: config.cvpChatCodeApiUrl,
4742
+ requestTimeoutMs: config.auth.requestTimeoutMs
4743
+ }, resolveReportingModel);
4744
+ });
4745
+ });
4746
+ let refreshQueued = false;
4747
+ let refreshInFlight;
4748
+ const clearManaged = () => {
4749
+ for (const registration of managedRegistrations.values()) registration();
4750
+ managedRegistrations.clear();
4751
+ managedSources.clear();
4752
+ };
4753
+ const refreshOnce = async () => {
4754
+ clearManaged();
4755
+ const resolved = current();
4756
+ const catalogs = [{
4757
+ provider: CODING_PLAN_PROVIDER,
4758
+ name: CODING_PLAN_PROVIDER_NAME,
4759
+ endpoint: resolved.codingPlanEndpoint,
4760
+ group: "codingplan"
4761
+ }];
4762
+ if (resolved.enableMaas) {
4763
+ if (resolved.maasEndpoint.trim() === "") ctx.logger.warn("chatcode-config: MAAS is enabled but no MAAS catalog endpoint is configured");
4764
+ else catalogs.push({
4765
+ provider: MAAS_PROVIDER,
4766
+ name: MAAS_PROVIDER_NAME,
4767
+ endpoint: resolved.maasEndpoint,
4768
+ group: "maas"
4769
+ });
4770
+ }
4771
+ const loaded = await Promise.all(catalogs.map(async (catalog) => {
4772
+ try {
4773
+ const models = catalog.group === "maas" ? await fetchMaasRuntimeModels(catalog.endpoint, resolved.catalogTimeoutMs, catalogAuthorization) : await fetchRuntimeModels(catalog.endpoint, resolved.catalogTimeoutMs, catalogAuthorization, (message) => ctx.logger.info(message));
4774
+ const source = resolveManagedSource(models, catalog.group, resolved);
4775
+ if (catalog.group === "codingplan") ctx.logger.info(`chatcode-config: CodingPlan registered ${String(source.selections.size)} runnable models from ${String(models.length)} parsed records`);
4776
+ return {
4777
+ catalog,
4778
+ source
4779
+ };
4780
+ } catch (error) {
4781
+ if (error instanceof LlmError) {
4782
+ ctx.logger.warn(`chatcode-config: ${catalog.name} models were not registered (${error.code})`);
4783
+ return;
4784
+ }
4785
+ throw error;
4786
+ }
4787
+ }));
4788
+ for (const entry of loaded) {
4789
+ if (entry === void 0) continue;
4790
+ const registration = register(entry.catalog.provider, entry.catalog.name, entry.source);
4791
+ if (registration !== void 0) {
4792
+ managedRegistrations.set(entry.catalog.provider, registration);
4793
+ managedSources.set(entry.catalog.provider, entry.source);
4794
+ }
4795
+ }
4796
+ };
4797
+ const refreshManaged = () => {
4798
+ if (refreshInFlight !== void 0) return refreshInFlight;
4799
+ const tracked = (async () => {
4800
+ do {
4801
+ refreshQueued = false;
4802
+ await refreshOnce();
4803
+ } while (refreshQueued);
4804
+ })().finally(() => {
4805
+ if (refreshInFlight === tracked) refreshInFlight = void 0;
4806
+ });
4807
+ refreshInFlight = tracked;
4808
+ return tracked;
4809
+ };
4810
+ const refreshForSettingsChange = () => {
4811
+ refreshCustom();
4812
+ if (refreshInFlight !== void 0) {
4813
+ refreshQueued = true;
4814
+ return;
4815
+ }
4816
+ refreshManaged();
4817
+ };
4818
+ const modelCatalogService = new ChatCodeModelCatalogService(ctx, refreshManaged, () => current().enableMaas);
4819
+ ctx.logger.info(`chatcode-config: waiting for settings service to register namespace "${SETTINGS_NAMESPACE}"`);
4820
+ ctx.inject(["settings"], async (settingsCtx) => {
4821
+ ctx.logger.info(`chatcode-config: settings service available; registering namespace "${SETTINGS_NAMESPACE}"`);
4822
+ try {
4823
+ settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
4824
+ setSource: (source) => {
4825
+ current = source;
4826
+ },
4827
+ onChange: refreshForSettingsChange,
4828
+ validate: (value) => {
4829
+ resolveConfiguredSource(value);
4830
+ }
4831
+ });
4832
+ } catch (error) {
4833
+ ctx.logger.warn(`chatcode-config: failed to register settings namespace "${SETTINGS_NAMESPACE}" (${error instanceof Error ? error.message : String(error)})`);
4834
+ throw error;
4835
+ }
4836
+ modelCatalogService.bindMaasSettings(async (enabled) => {
4837
+ ctx.logger.info(`chatcode-config: persisting MAAS setting enableMaas=${String(enabled)}`);
4838
+ const ops = [{
4839
+ op: "set",
4840
+ path: ["enableMaas"],
4841
+ value: enabled
4842
+ }];
4843
+ const revision = () => settingsCtx.settings.describe().find((entry) => entry.ns === SETTINGS_NAMESPACE)?.revision;
4844
+ try {
4845
+ await settingsCtx.settings.mutate(SETTINGS_NAMESPACE, ops, revision());
4846
+ } catch (error) {
4847
+ if (error?.code !== "SETTINGS_CONFLICT") throw error;
4848
+ await settingsCtx.settings.mutate(SETTINGS_NAMESPACE, ops, revision());
4849
+ }
4850
+ ctx.logger.info(`chatcode-config: persisted MAAS setting enableMaas=${String(enabled)}`);
4851
+ });
4852
+ const descriptor = settingsCtx.settings.describe().find((entry) => entry.ns === SETTINGS_NAMESPACE);
4853
+ ctx.logger.info(`chatcode-config: settings namespace "${SETTINGS_NAMESPACE}" registered=${String(descriptor !== void 0)} revision=${String(descriptor?.revision ?? "missing")} enableMaas=${String(current().enableMaas)}`);
4854
+ if (descriptor?.user !== void 0) {
4855
+ ctx.logger.info(`chatcode-config: settings namespace "${SETTINGS_NAMESPACE}" loaded an existing user section`);
4856
+ return;
4857
+ }
4858
+ try {
4859
+ const imported = await readSource(config);
4860
+ await settingsCtx.settings.replace(SETTINGS_NAMESPACE, { customModels: imported.entries ?? [] });
4861
+ ctx.logger.info(`chatcode-config: settings namespace "${SETTINGS_NAMESPACE}" initialized from the legacy source`);
4862
+ } catch (error) {
4863
+ if (!(error instanceof LlmError)) throw error;
4864
+ ctx.logger.warn("chatcode-config: legacy settingsPath is unavailable or invalid; custom models were not imported and import will retry when the Host starts again");
4865
+ }
4866
+ });
4867
+ refreshCustom();
4868
+ await refreshManaged();
4869
+ }
4870
+ //#endregion
4871
+ export { ChatCodeAuthService, ChatCodeModelCatalogService, ChatCodeStartupGateService, Config, SETTINGS_NAMESPACE, apply, checkChatCodeStartupGate, inject, name, startupGateFailureMessage };
4872
+
4873
+ //# sourceMappingURL=index.js.map