@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
@@ -0,0 +1,164 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { homedir } from "node:os";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ //#region src/startup-gate.ts
6
+ /** Host-only ChatCode launch admission through the CVP system configuration API. */
7
+ const ENABLED_KEY = "chatcode.cli.isEnabled";
8
+ const DISABLED_REASON_KEY = "chatcode.cli.disabled.reason";
9
+ const CVP_CHATCODE_API_TOKEN = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjhlOGM2NDkxLTBmMjUtNGQ3Ni1iOWRmLTQwZjZiYzk1M2E1MSJ9.8A07cEQ8YjHhVCRhO1U8T8LLmB2Ttf7ZrElCXqEYZw3pWGWFv6PwQUbM5FVlhBaZggAkqTswSezqqccB8bVeqQ";
10
+ const DEFAULT_DISABLED_REASON = "系统管理员已暂停使用,请联系管理员。";
11
+ function enabledValue(data) {
12
+ if (data === null || typeof data !== "object" || !("msg" in data)) return void 0;
13
+ const value = data.msg;
14
+ if (typeof value === "boolean") return value;
15
+ if (typeof value !== "string") return void 0;
16
+ switch (value.trim().toLowerCase()) {
17
+ case "true": return true;
18
+ case "false": return false;
19
+ default: return;
20
+ }
21
+ }
22
+ function disabledReason(data) {
23
+ const value = data !== null && typeof data === "object" && "msg" in data ? data.msg : void 0;
24
+ if (typeof value !== "string") return DEFAULT_DISABLED_REASON;
25
+ const printable = Array.from(value).filter((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127).join("").replace(/\s+/gu, " ").trim();
26
+ return printable ? printable.slice(0, 500) : DEFAULT_DISABLED_REASON;
27
+ }
28
+ function requestFailure(error) {
29
+ const code = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code.toUpperCase() : "";
30
+ if ([
31
+ "ETIMEDOUT",
32
+ "ESOCKETTIMEDOUT",
33
+ "ECONNABORTED",
34
+ "ERR_CONNECTION_TIMED_OUT"
35
+ ].includes(code) || error instanceof DOMException && ["TimeoutError", "AbortError"].includes(error.name)) return "timeout";
36
+ return "network";
37
+ }
38
+ function truthy(value) {
39
+ return value !== void 0 && [
40
+ "1",
41
+ "true",
42
+ "yes",
43
+ "on"
44
+ ].includes(value.trim().toLowerCase());
45
+ }
46
+ async function legacySettings(config) {
47
+ const filename = resolve(config.settingsPath ?? join(homedir(), ".chatcode-cli", "settings.json"));
48
+ return JSON.parse(await readFile(filename, "utf8"));
49
+ }
50
+ async function bypassed(config, options) {
51
+ if (truthy((options.environment ?? process.env).CHATCODE_CLI_DEGRADE_STARTUP_GATE)) return true;
52
+ try {
53
+ const settings = await (options.readSettings ?? (() => legacySettings(config)))();
54
+ if (settings === null || typeof settings !== "object") return false;
55
+ const record = settings;
56
+ if (record.CHATCODE_CLI_NEED_AUTH_YZQ !== void 0) return record.CHATCODE_CLI_NEED_AUTH_YZQ === false;
57
+ return record.WANMA_CLI_NEED_AUTH_YZQ === false;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function endpoint(baseUrl) {
63
+ const url = new URL(baseUrl);
64
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && [
65
+ "localhost",
66
+ "127.0.0.1",
67
+ "[::1]"
68
+ ].includes(url.hostname))) throw new Error("ChatCode startup gate requires an HTTPS CVP URL (HTTP is allowed only on loopback).");
69
+ return url;
70
+ }
71
+ function configUrl(base, key) {
72
+ return `${base.href.replace(/\/+$/u, "")}/system/config/configKey/${encodeURIComponent(key)}`;
73
+ }
74
+ /** Check one launch. Each call contacts CVP unless the explicit legacy bypass applies. */
75
+ async function checkChatCodeStartupGate(config, options = {}) {
76
+ const base = endpoint(config.cvpChatCodeApiUrl);
77
+ const host = base.hostname;
78
+ if (await bypassed(config, options)) return {
79
+ ok: true,
80
+ host
81
+ };
82
+ const token = config.startupGate.token.trim() || (options.environment ?? process.env).CHATCODE_CVP_CONFIG_TOKEN?.trim() || CVP_CHATCODE_API_TOKEN;
83
+ const authorization = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
84
+ const request = options.request ?? fetch;
85
+ const get = (key) => request(configUrl(base, key), {
86
+ method: "GET",
87
+ headers: {
88
+ Authorization: authorization,
89
+ Accept: "application/json"
90
+ },
91
+ signal: AbortSignal.timeout(config.startupGate.timeoutMs)
92
+ });
93
+ let response;
94
+ try {
95
+ response = await get(ENABLED_KEY);
96
+ } catch (error) {
97
+ return {
98
+ ok: false,
99
+ kind: "error",
100
+ host,
101
+ reason: requestFailure(error)
102
+ };
103
+ }
104
+ if (!response.ok) return {
105
+ ok: false,
106
+ kind: "error",
107
+ host,
108
+ reason: response.status === 401 || response.status === 403 ? "auth" : response.status >= 500 ? "server" : "invalid",
109
+ status: response.status
110
+ };
111
+ const value = enabledValue(await response.json().catch(() => void 0));
112
+ if (value === void 0) return {
113
+ ok: false,
114
+ kind: "error",
115
+ host,
116
+ reason: "invalid"
117
+ };
118
+ if (value) return {
119
+ ok: true,
120
+ host
121
+ };
122
+ let reason = DEFAULT_DISABLED_REASON;
123
+ try {
124
+ const detail = await get(DISABLED_REASON_KEY);
125
+ if (detail.ok) reason = disabledReason(await detail.json().catch(() => void 0));
126
+ } catch {}
127
+ return {
128
+ ok: false,
129
+ kind: "disabled",
130
+ host,
131
+ disabledReason: reason
132
+ };
133
+ }
134
+ /** Localize a failed launch without disclosing the CVP credential or response body. */
135
+ function startupGateFailureMessage(failure) {
136
+ if (failure.kind === "disabled") return `ChatCode CLI 当前不可用。\n原因:${failure.disabledReason}`;
137
+ switch (failure.reason) {
138
+ case "auth": return `无法验证 ChatCode 可用状态(${failure.host}):启动配置凭证无效。`;
139
+ case "timeout": return "ChatCode 服务连接超时,请检查网络连接后重试。";
140
+ case "network": return "无法连接到 ChatCode 服务,请检查网络连接后重试。";
141
+ case "server": return `ChatCode 配置服务暂时不可用(${failure.host}),请稍后重试。`;
142
+ case "invalid": return `ChatCode 启用状态配置无效(${failure.host}),请联系管理员。`;
143
+ }
144
+ }
145
+ /** Reusable Host service for CLI startup and Web boot admission. */
146
+ var ChatCodeStartupGateService = class extends Service {
147
+ config;
148
+ constructor(ctx, config) {
149
+ super(ctx, "chatcodeStartupGate");
150
+ this.config = config;
151
+ }
152
+ /** Read one authoritative CVP launch decision. */
153
+ check() {
154
+ return checkChatCodeStartupGate(this.config);
155
+ }
156
+ /** Present a failed decision without publishing credentials or raw response bodies. */
157
+ message(failure) {
158
+ return startupGateFailureMessage(failure);
159
+ }
160
+ };
161
+ //#endregion
162
+ export { checkChatCodeStartupGate as n, startupGateFailureMessage as r, ChatCodeStartupGateService as t };
163
+
164
+ //# sourceMappingURL=startup-gate-BaCbWaKH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup-gate-BaCbWaKH.js","names":[],"sources":["../src/startup-gate.ts"],"sourcesContent":["/** Host-only ChatCode launch admission through the CVP system configuration API. */\nimport { readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { Service } from '@deepseek-ai/cordis'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Config } from './config.ts'\n\nconst ENABLED_KEY = 'chatcode.cli.isEnabled'\nconst DISABLED_REASON_KEY = 'chatcode.cli.disabled.reason'\nconst CVP_CHATCODE_API_TOKEN = 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjhlOGM2NDkxLTBmMjUtNGQ3Ni1iOWRmLTQwZjZiYzk1M2E1MSJ9.8A07cEQ8YjHhVCRhO1U8T8LLmB2Ttf7ZrElCXqEYZw3pWGWFv6PwQUbM5FVlhBaZggAkqTswSezqqccB8bVeqQ'\nconst DEFAULT_DISABLED_REASON = '系统管理员已暂停使用,请联系管理员。'\n\nexport type StartupGateFailure =\n | { ok: false; kind: 'disabled'; host: string; disabledReason: string }\n | { ok: false; kind: 'error'; host: string; reason: 'auth' | 'timeout' | 'network' | 'server' | 'invalid'; status?: number }\nexport type StartupGateResult = { ok: true; host: string } | StartupGateFailure\n\nexport interface StartupGateCheckOptions {\n request?: typeof fetch\n environment?: NodeJS.ProcessEnv\n readSettings?: () => Promise<unknown>\n}\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context { chatcodeStartupGate: ChatCodeStartupGateService }\n}\n\nfunction enabledValue(data: unknown): boolean | undefined {\n if (data === null || typeof data !== 'object' || !('msg' in data)) return undefined\n const value = data.msg\n if (typeof value === 'boolean') return value\n if (typeof value !== 'string') return undefined\n switch (value.trim().toLowerCase()) {\n case 'true': return true\n case 'false': return false\n default: return undefined\n }\n}\n\nfunction disabledReason(data: unknown): string {\n const value = data !== null && typeof data === 'object' && 'msg' in data ? data.msg : undefined\n if (typeof value !== 'string') return DEFAULT_DISABLED_REASON\n const printable = Array.from(value)\n .filter(character => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127)\n .join('').replace(/\\s+/gu, ' ').trim()\n return printable ? printable.slice(0, 500) : DEFAULT_DISABLED_REASON\n}\n\nfunction requestFailure(error: unknown): Extract<StartupGateFailure, { kind: 'error' }>['reason'] {\n const code = error !== null && typeof error === 'object' && 'code' in error && typeof error.code === 'string'\n ? error.code.toUpperCase() : ''\n if (['ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNABORTED', 'ERR_CONNECTION_TIMED_OUT'].includes(code)\n || error instanceof DOMException && ['TimeoutError', 'AbortError'].includes(error.name)) return 'timeout'\n return 'network'\n}\n\nfunction truthy(value: string | undefined): boolean {\n return value !== undefined && ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase())\n}\n\nasync function legacySettings(config: Config): Promise<unknown> {\n const filename = resolve(config.settingsPath ?? join(homedir(), '.chatcode-cli', 'settings.json'))\n return JSON.parse(await readFile(filename, 'utf8')) as unknown\n}\n\nasync function bypassed(config: Config, options: StartupGateCheckOptions): Promise<boolean> {\n if (truthy((options.environment ?? process.env).CHATCODE_CLI_DEGRADE_STARTUP_GATE)) return true\n try {\n const settings = await (options.readSettings ?? (() => legacySettings(config)))()\n if (settings === null || typeof settings !== 'object') return false\n const record = settings as Record<string, unknown>\n if (record.CHATCODE_CLI_NEED_AUTH_YZQ !== undefined) return record.CHATCODE_CLI_NEED_AUTH_YZQ === false\n return record.WANMA_CLI_NEED_AUTH_YZQ === false\n } catch {\n // Missing or malformed legacy settings cannot silently bypass launch admission.\n return false\n }\n}\n\nfunction endpoint(baseUrl: string): URL {\n const url = new URL(baseUrl)\n if (url.username || url.password || url.search || url.hash\n || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) {\n throw new Error('ChatCode startup gate requires an HTTPS CVP URL (HTTP is allowed only on loopback).')\n }\n return url\n}\n\nfunction configUrl(base: URL, key: string): string {\n return `${base.href.replace(/\\/+$/u, '')}/system/config/configKey/${encodeURIComponent(key)}`\n}\n\n/** Check one launch. Each call contacts CVP unless the explicit legacy bypass applies. */\nexport async function checkChatCodeStartupGate(config: Config, options: StartupGateCheckOptions = {}): Promise<StartupGateResult> {\n const base = endpoint(config.cvpChatCodeApiUrl)\n const host = base.hostname\n if (await bypassed(config, options)) return { ok: true, host }\n const token = config.startupGate.token.trim()\n || (options.environment ?? process.env).CHATCODE_CVP_CONFIG_TOKEN?.trim()\n || CVP_CHATCODE_API_TOKEN\n const authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`\n const request = options.request ?? fetch\n const get = (key: string): Promise<Response> => request(configUrl(base, key), {\n method: 'GET',\n headers: { Authorization: authorization, Accept: 'application/json' },\n signal: AbortSignal.timeout(config.startupGate.timeoutMs),\n })\n\n let response: Response\n try { response = await get(ENABLED_KEY) }\n catch (error) { return { ok: false, kind: 'error', host, reason: requestFailure(error) } }\n if (!response.ok) {\n const reason = response.status === 401 || response.status === 403 ? 'auth'\n : response.status >= 500 ? 'server' : 'invalid'\n return { ok: false, kind: 'error', host, reason, status: response.status }\n }\n const value = enabledValue(await response.json().catch(() => undefined))\n if (value === undefined) return { ok: false, kind: 'error', host, reason: 'invalid' }\n if (value) return { ok: true, host }\n\n let reason = DEFAULT_DISABLED_REASON\n try {\n const detail = await get(DISABLED_REASON_KEY)\n if (detail.ok) reason = disabledReason(await detail.json().catch(() => undefined))\n } catch {\n // A missing reason does not change the authoritative disabled result.\n }\n return { ok: false, kind: 'disabled', host, disabledReason: reason }\n}\n\n/** Localize a failed launch without disclosing the CVP credential or response body. */\nexport function startupGateFailureMessage(failure: StartupGateFailure): string {\n if (failure.kind === 'disabled') return `ChatCode CLI 当前不可用。\\n原因:${failure.disabledReason}`\n switch (failure.reason) {\n case 'auth': return `无法验证 ChatCode 可用状态(${failure.host}):启动配置凭证无效。`\n case 'timeout': return 'ChatCode 服务连接超时,请检查网络连接后重试。'\n case 'network': return '无法连接到 ChatCode 服务,请检查网络连接后重试。'\n case 'server': return `ChatCode 配置服务暂时不可用(${failure.host}),请稍后重试。`\n case 'invalid': return `ChatCode 启用状态配置无效(${failure.host}),请联系管理员。`\n }\n}\n\n/** Reusable Host service for CLI startup and Web boot admission. */\nexport class ChatCodeStartupGateService extends Service {\n constructor(ctx: Context, private readonly config: Config) { super(ctx, 'chatcodeStartupGate') }\n\n /** Read one authoritative CVP launch decision. */\n check(): Promise<StartupGateResult> { return checkChatCodeStartupGate(this.config) }\n\n /** Present a failed decision without publishing credentials or raw response bodies. */\n message(failure: StartupGateFailure): string { return startupGateFailureMessage(failure) }\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/web-startup.d.ts
3
+ declare const name = "chatcode-web-startup";
4
+ declare const inject: string[];
5
+ /** Provide Web readiness after CVP permits launch; known refusals print and request a clean exit. */
6
+ declare function apply(ctx: Context): Promise<void>;
7
+ //#endregion
8
+ export { apply, inject, name };
9
+ //# sourceMappingURL=web-startup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-startup.d.ts","names":[],"sources":["../src/web-startup.ts"],"mappings":";;cAMa;cACA;;iBAGS,MAAM,KAAK,UAAU"}
@@ -0,0 +1,20 @@
1
+ import { r as startupGateFailureMessage } from "./startup-gate-BaCbWaKH.js";
2
+ //#region src/web-startup.ts
3
+ const name = "chatcode-web-startup";
4
+ const inject = ["chatcodeStartupGate"];
5
+ /** Provide Web readiness after CVP permits launch; known refusals print and request a clean exit. */
6
+ async function apply(ctx) {
7
+ const result = await ctx.chatcodeStartupGate.check();
8
+ if (!result.ok) {
9
+ const exit = ctx.get("appExit");
10
+ if (exit === void 0) throw new Error("chatcode-web-startup: the launcher must provide ctx.appExit before the tree mounts");
11
+ process.stdout.write(`${startupGateFailureMessage(result)}\n`);
12
+ exit(0);
13
+ return;
14
+ }
15
+ ctx.provide("chatcodeStartupReady", true);
16
+ }
17
+ //#endregion
18
+ export { apply, inject, name };
19
+
20
+ //# sourceMappingURL=web-startup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-startup.js","names":[],"sources":["../src/web-startup.ts"],"sourcesContent":["/** Web boot admission waits for the shared ChatCode launch decision. */\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-cmdline'\nimport type {} from './startup-gate.ts'\nimport { startupGateFailureMessage } from './startup-gate.ts'\n\nexport const name = 'chatcode-web-startup'\nexport const inject = ['chatcodeStartupGate']\n\n/** Provide Web readiness after CVP permits launch; known refusals print and request a clean exit. */\nexport async function apply(ctx: Context): Promise<void> {\n const result = await ctx.chatcodeStartupGate.check()\n if (!result.ok) {\n const exit = ctx.get('appExit')\n if (exit === undefined) {\n throw new Error('chatcode-web-startup: the launcher must provide ctx.appExit before the tree mounts')\n }\n process.stdout.write(`${startupGateFailureMessage(result)}\\n`)\n exit(0)\n return\n }\n ctx.provide('chatcodeStartupReady', true)\n}\n"],"mappings":";;AAMA,MAAa,OAAO;AACpB,MAAa,SAAS,CAAC,qBAAqB;;AAG5C,eAAsB,MAAM,KAA6B;CACvD,MAAM,SAAS,MAAM,IAAI,oBAAoB,MAAM;CACnD,IAAI,CAAC,OAAO,IAAI;EACd,MAAM,OAAO,IAAI,IAAI,SAAS;EAC9B,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,oFAAoF;EAEtG,QAAQ,OAAO,MAAM,GAAG,0BAA0B,MAAM,EAAE,GAAG;EAC7D,KAAK,CAAC;EACN;CACF;CACA,IAAI,QAAQ,wBAAwB,IAAI;AAC1C"}
package/package.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "@chatcode/cco-llm-chatcode-config",
3
+ "description": "ChatCode CLI plugin for ChatCode models, shared account access, and operations reporting",
4
+ "version": "0.1.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://gitee.com/shiguifeng/dsh-llm-chatcode-config.git"
8
+ },
9
+ "type": "module",
10
+ "main": "./lib/index.js",
11
+ "types": "./lib/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/index.d.ts",
15
+ "default": "./lib/index.js"
16
+ },
17
+ "./client": "./lib/client.js",
18
+ "./web-startup": {
19
+ "types": "./lib/web-startup.d.ts",
20
+ "default": "./lib/web-startup.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "cordis.patch.yml",
27
+ "cordis.web.patch.yml",
28
+ "docs",
29
+ "LICENSE",
30
+ "vendor"
31
+ ],
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=22.19.0"
35
+ },
36
+ "dsh": {
37
+ "bundle": {
38
+ "patch": "./cordis.patch.yml"
39
+ },
40
+ "client": {
41
+ "inject": [
42
+ "@deepseek-ai/dsh-api-remotes",
43
+ "@deepseek-ai/dsh-client-locale",
44
+ "@deepseek-ai/dsh-client-ui-settings-models"
45
+ ],
46
+ "platform": "web"
47
+ }
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run",
53
+ "prepack": "pnpm run build",
54
+ "verify:brand": "node scripts/verify-brand.mjs"
55
+ },
56
+ "peerDependencies": {
57
+ "@deepseek-ai/cordis": "^4.0.2",
58
+ "@deepseek-ai/dsh-anonymous-user-id": ">=0.1.6-alpha.1 <0.2.0",
59
+ "@deepseek-ai/dsh-attachment": ">=0.1.6-alpha.1 <0.2.0",
60
+ "@deepseek-ai/dsh-authorization": ">=0.1.6-alpha.1 <0.2.0",
61
+ "@deepseek-ai/dsh-cmdline": ">=0.1.6-alpha.1 <0.2.0",
62
+ "@deepseek-ai/dsh-commands": ">=0.1.6-alpha.1 <0.2.0",
63
+ "@deepseek-ai/dsh-fs": ">=0.1.6-alpha.1 <0.2.0",
64
+ "@deepseek-ai/dsh-home-paths": ">=0.1.6-alpha.1 <0.2.0",
65
+ "@deepseek-ai/dsh-invariants": ">=0.1.6-alpha.1 <0.2.0",
66
+ "@deepseek-ai/dsh-llm": ">=0.1.6-alpha.1 <0.2.0",
67
+ "@deepseek-ai/dsh-llm-deepseek": ">=0.1.6-alpha.1 <0.2.0",
68
+ "@deepseek-ai/dsh-session": ">=0.1.6-alpha.1 <0.2.0",
69
+ "@deepseek-ai/dsh-settings": ">=0.1.6-alpha.1 <0.2.0",
70
+ "@deepseek-ai/dsh-tools": ">=0.1.6-alpha.1 <0.2.0",
71
+ "@earendil-works/pi-ai": "^0.84.2"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@earendil-works/pi-ai": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "dependencies": {
79
+ "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
80
+ "@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
81
+ "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
82
+ "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
83
+ "@deepseek-ai/dsh-util-values": "0.1.6-alpha.1",
84
+ "@deepseek-ai/schemastery": "3.18.2"
85
+ },
86
+ "devDependencies": {
87
+ "@deepseek-ai/cordis": "4.0.2",
88
+ "@deepseek-ai/cordis-plugin-include": "1.0.7",
89
+ "@deepseek-ai/cordis-plugin-loader": "1.0.3",
90
+ "@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
91
+ "@deepseek-ai/dsh-authorization": "0.1.6-alpha.1",
92
+ "@deepseek-ai/dsh-anonymous-user-id": "0.1.6-alpha.1",
93
+ "@deepseek-ai/dsh-agent": "0.1.6-alpha.1",
94
+ "@deepseek-ai/dsh-atomic-write": "0.1.6-alpha.1",
95
+ "@deepseek-ai/dsh-cmdline": "0.1.6-alpha.1",
96
+ "@deepseek-ai/dsh-commands": "0.1.6-alpha.1",
97
+ "@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
98
+ "@deepseek-ai/dsh-invariants": "0.1.6-alpha.1",
99
+ "@deepseek-ai/dsh-credentials-local": "0.1.6-alpha.1",
100
+ "@deepseek-ai/dsh-home-paths": "0.1.6-alpha.1",
101
+ "@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
102
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.6-alpha.1",
103
+ "@deepseek-ai/dsh-ptc-runtime": "0.1.6-alpha.1",
104
+ "@deepseek-ai/dsh-sandbox": "0.1.6-alpha.1",
105
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.6-alpha.1",
106
+ "@deepseek-ai/dsh-scope": "0.1.6-alpha.1",
107
+ "@deepseek-ai/dsh-session": "0.1.6-alpha.1",
108
+ "@deepseek-ai/dsh-session-projection": "0.1.6-alpha.1",
109
+ "@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
110
+ "@deepseek-ai/dsh-settings-file": "0.1.6-alpha.1",
111
+ "@deepseek-ai/dsh-system-prompt": "0.1.6-alpha.1",
112
+ "@deepseek-ai/dsh-tools": "0.1.6-alpha.1",
113
+ "@deepseek-ai/dsh-user-approval": "0.1.6-alpha.1",
114
+ "@earendil-works/pi-ai": "0.84.2",
115
+ "@types/node": "^24.0.0",
116
+ "react": "^18.3.1",
117
+ "tsdown": "^0.22.2",
118
+ "typescript": "^5.9.0",
119
+ "vitest": "^4.0.0"
120
+ }
121
+ }
@@ -0,0 +1,7 @@
1
+ # Vendored source
2
+
3
+ `dsh-llm-pi-ai/` is a complete source, test, manifest, and documentation copy of `@deepseek-ai/dsh-llm-pi-ai` 0.1.2-alpha.3 from the DeepSeek Harness checkout used to build this plugin. The copy includes the request-authentication and `reasoning_split` changes required by the ChatCode adapter. Its request-image projection uses the `0.1.6-alpha.1` attachment API so the published plugin shares one Harness package generation with the native DeepSeek adapter.
4
+
5
+ The plugin imports the copied adapter, authentication, and profile-resolution modules by relative path. It never imports or mounts the installed `@deepseek-ai/dsh-llm-pi-ai` package. `dsh-llm-pi-ai/tsconfig.upstream.json` preserves the monorepo project configuration; `tsconfig.json` is the standalone test adapter. Keep the source copy intact when updating it, record the new upstream version here, and reapply the ChatCode-specific changes before publishing.
6
+
7
+ The copied package and DeepSeek Harness are distributed under the MIT License. See [`dsh-llm-pi-ai/LICENSE`](dsh-llm-pi-ai/LICENSE).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
5
+ README.md: fea72b2acd0d9ec98347588b66e8899af1599c2a
6
+ README.zh.md: d52b22e816cb2fb62ad6bf83b89b436455876e60
@@ -0,0 +1,238 @@
1
+ ---
2
+ description: "The pi-ai-backed multi-provider adapter for users and maintainers routing the harness LLM service through pi-ai catalogs and hand-declared gateways."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-llm-pi-ai
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `@deepseek-ai/dsh-llm-pi-ai` is the pi-ai-backed multi-provider adapter for the harness LLM service: one plugin instance owns a dictionary of provider routes, each served through [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai). A route naming an installed pi-ai provider inherits its endpoint, wire protocol, and model catalog as defaults; a route pi-ai does not ship is declared outright, so an OpenAI-compatible gateway or self-hosted server is configuration, not a code change. Profiles and credentials resolve per request over the optional settings and credential seams, so editing the user settings document changes the next request without a restart. A provider that ships a login can be signed into through the harness authorization seam, and the stored sign-in — an OAuth grant, or a key typed into pi-ai's own login prompt — authenticates its route and refreshes itself under the store's cross-process lock. The plugin can mount dormant with zero routes and activate them the moment a settings section supplies profiles.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin when a composition routes model requests through pi-ai's provider catalogs or through gateways that pi-ai's installed catalog does not describe. The `providers` dictionary is the whole configuration surface: each key is the provider route name a request selects with `GenerateOptions.provider`.
29
+
30
+ ### When to choose it
31
+
32
+ Choose this adapter when the same composition serves several providers, when a route needs pi-ai's catalog defaults with a few fields corrected, or when a hand-declared gateway must be reached through its own endpoint and protocol. Choose `dsh-llm-deepseek` for the direct DeepSeek route when the deployment needs no other provider. Both adapters can be mounted together because their route names do not collide; registering a route another adapter already owns fails plugin loading.
33
+
34
+ ### Configure provider routes
35
+
36
+ Each profile may set a `retryPolicy`; omission uses normal mode with five retries. `apiKeyEnv` is a credential reference resolved per request through the harness credential seam, so no secret enters the configuration file; a reference that resolves to nothing fails the request with `MISSING_CREDENTIAL`. Omitting it leaves the route configured-but-keyless, which for an installed catalog route defers to pi-ai's provider-native ambient discovery.
37
+
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-llm-pi-ai'
40
+ config:
41
+ providers:
42
+ openai:
43
+ apiKeyEnv: OPENAI_API_KEY
44
+ baseURL: https://proxy.example.com:8443
45
+ reasoning: high
46
+ requestImagePixelBudget: 4194304 # total pixels; 2048 by 2048 default
47
+ requestImageMaxBytes: 1048576 # raw bytes before base64 expansion
48
+ maxRequestImageBytes: 20971520 # accumulated base64 payload
49
+ retryPolicy:
50
+ mode: normal
51
+ maxRetries: 3
52
+ anthropic:
53
+ apiKeyEnv: ANTHROPIC_API_KEY
54
+ models:
55
+ - id: claude-sonnet-4-5
56
+ contextWindow: 200000
57
+ acme-gateway:
58
+ displayName: Acme Gateway
59
+ apiKeyEnv: ACME_GATEWAY_API_KEY
60
+ api: openai-completions
61
+ baseURL: https://gateway.acme.example/v1
62
+ compat:
63
+ thinkingFormat: deepseek
64
+ models:
65
+ - id: acme-think
66
+ name: Acme Think
67
+ contextWindow: 262144
68
+ reasoningEfforts:
69
+ off:
70
+ high: high
71
+ ```
72
+
73
+ | Field | Default | Meaning |
74
+ |---|---|---|
75
+ | `apiKeyEnv` | absent | Credential reference resolved per request; omission defers to pi-ai ambient discovery |
76
+ | `displayName` | provider name | Label shown by selector surfaces |
77
+ | `api` | catalog protocol | Wire protocol; only needed for routes the catalog does not supply |
78
+ | `baseURL` | catalog endpoint | Endpoint of every model on the route |
79
+ | `models` | installed catalog | Replaces the route's catalog wholesale; each entry defaults from the installed model |
80
+ | `modelOverrides` | none | Reshapes individual installed-catalog models without replacing the rest |
81
+ | `compat` | catalog detection | Wire-compatibility switches for unrecognized endpoints |
82
+ | `defaultContextWindow` | `262,144` | Capacity fallback for undescribed models |
83
+ | `defaultMaxTokens` | `32,768` | Output-cap fallback for undescribed models |
84
+ | `requestImagePixelBudget` | `4,194,304` | Total-pixel budget for each deterministic request image |
85
+ | `requestImageMaxBytes` | `1 MiB` | Encoded-byte target for each request image before base64 expansion |
86
+ | `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound with oldest-first offload |
87
+ | `retryPolicy` | normal, 5 retries | Provider-owned retry policy executed by `dsh-llm-retry` |
88
+
89
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) is the exhaustive source for every accepted field and its JSDoc.
90
+
91
+ ### Sign in to a provider
92
+
93
+ A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
94
+
95
+ ### Resolve the model catalog
96
+
97
+ A profile's `models` list replaces the route's installed catalog rather than extending it; each entry defaults its unset fields from the installed model of the same id, so narrowing a route to two models, correcting one capacity, or adding a model newer than the installed catalog are one-line edits. `modelOverrides` reshapes individual installed-catalog models without that cost — correct one model, keep the other thirty-seven — and is refused when set beside a `models` list, on a hand-declared route, or naming a model the catalog does not describe, because a silently unchanged model would be a typo someone hunts for later.
98
+
99
+ ### Run with reasoning and wire compatibility
100
+
101
+ `reasoningEfforts` declares a model's selectable thinking levels: each key is a level selectors offer, its value the spelling dispatch sends on the wire, so `max: ultra` renames a level for a gateway with its own vocabulary. Omitting the field keeps the installed catalog entry's capability; `false` declares a non-reasoning model. `compat` switches reshape the request for endpoints pi-ai cannot recognize — which role carries the system prompt, which field caps output, how a thinking level travels — configurable per route and per model. A model neither the entry nor the installed catalog sizes takes the route's `defaultContextWindow` and `defaultMaxTokens` fallbacks.
102
+
103
+ For OpenAI Chat Completions gateways that support it, set profile `reasoningSplit: true` to request `reasoning_split` and separate reasoning from answer text. `false` explicitly requests the unsplit format; omission leaves the gateway default. Other protocols reject the field. This controls response formatting, independently of reasoning effort.
104
+
105
+ ### Change configuration at runtime
106
+
107
+ Profiles are re-read once per operation through the optional settings seam: the base and the user's `llm-pi-ai:` settings section merge per provider, so a user can add a route, override one field of a composition route, or point a route at another proxy, all effective on the next request with no restart. A section the adapter could not serve is refused where it is written — `settings.mutate` answers `settings-rejected` — and a stored section that later fails keeps the namespace's last good value. When the route set or a route's retry policy changes, the plugin re-registers atomically: a conflicting route leaves the previous routes serving.
108
+
109
+ ### Discover models from endpoints
110
+
111
+ The plugin answers "which models can this provider serve?" for a route a configuration surface is editing or drafting. A route the installed catalog ships is answered from that catalog with no network call; only a route the catalog does not describe is interrogated over the wire (`openai-completions` and `openai-responses` shapes). The reply is candidate metadata a surface may offer for adoption — nothing is stored, and `settings.yaml` remains the only thing that decides what a route serves.
112
+
113
+ ### Failures and recovery
114
+
115
+ A route pi-ai does not ship needs `api`, `baseURL`, and a non-empty `models` list; an unserviceable profile is refused where it is written, naming the route and model. Failures carry stable codes: a credential that cannot be used fails with `INVALID_CREDENTIAL` naming the route and reference, a route whose `apiKeyEnv` reference resolves to nothing fails with `MISSING_CREDENTIAL`, an unconfigured model fails with `UNKNOWN_MODEL`, and terminal provider failures distinguish `QUOTA` from transient `RATE_LIMIT`. `GenerateOptions.stop` is rejected with `UNSUPPORTED_OPTION` because pi-ai's common streaming UI cannot guarantee it across providers.
116
+
117
+ -----
118
+
119
+ <a id="understand-the-implementation"></a>
120
+ ## Understand the implementation
121
+
122
+ <details>
123
+ <summary>Implementation internals — click to expand</summary>
124
+
125
+ This section explains the design behind the adapter; the observable behavior is fully covered in [Use this package](#use-this-package).
126
+
127
+ ### Design philosophy
128
+
129
+ Other configuration-source plugins can construct `PiAiAdapter` with resolved profiles, private request authentication, and isolated credential storage. The [ChatCode adapter](../llm-chatcode-config/README.md) uses this extension without mounting the pi-ai settings plugin.
130
+
131
+ The adapter is built on immutable snapshots and per-operation resolution. Each operation captures a whole snapshot — the profiles plus a `createModels()` collection holding the `Provider` each route built — before its first `await`, and a configuration change builds a new collection rather than mutating the one in use, so a request that started under one configuration never finishes under another. A route's own credential reference resolves through the harness seam and rides as the request's `apiKey` option, which pi-ai treats as the highest-priority auth override — that is what keeps the fail-loud reference semantics. Everything that override does not cover reaches pi-ai through the collection's own auth: the credential store holds the records a login wrote and a refresh rotates (addressed as `llm-pi-ai/<provider id>`), and the auth context answers the ambient questions a provider asks while resolving. Both are stable across snapshots, so a configuration change rebuilds the collection without forgetting who is signed in.
132
+
133
+ ### Source map
134
+
135
+ | File | Role |
136
+ |---|---|
137
+ | [`src/index.ts`](src/index.ts) | Plugin entry: profile resolution, settings wiring, directory and route registration |
138
+ | [`src/auth.ts`](src/auth.ts) | The credential store and ambient auth context over the harness credential plane |
139
+ | [`src/login.ts`](src/login.ts) | Authorization flows for the installed providers that ship a login |
140
+ | [`src/config.ts`](src/config.ts) | Profile schema, resolution, and serviceability checks |
141
+ | [`src/catalog.ts`](src/catalog.ts) | Installed-catalog integration and drift gates |
142
+ | [`src/provider.ts`](src/provider.ts) | The supported-protocol table and provider construction |
143
+ | [`src/context.ts`](src/context.ts) | ChatCode CLI-to-pi-ai context conversion, image handling, replay restore |
144
+ | [`src/stream.ts`](src/stream.ts) | pi-ai event conversion into harness `StreamChunk` values |
145
+ | [`src/replay.ts`](src/replay.ts) | Versioned `ReplayEnvelope` storage and validation |
146
+ | [`src/discovery.ts`](src/discovery.ts) | Endpoint interrogation for configuration surfaces |
147
+
148
+ ### Registration and directory
149
+
150
+ The plugin declares every installed catalog provider it can authenticate in the configurable-provider directory, joined with every route the current profiles declare, so configuration surfaces can offer the full catalog before any route exists. Each entry carries `declared` — whether pi-ai ships nothing under that key — because only the adapter can distinguish a hand-declared route from a narrowed catalog route. Route registration is atomic: a candidate set that collides with another adapter leaves the previous routes serving. A bare mount with zero routes is the dormant posture: nothing registers until a settings section supplies profiles, and routes drop when it empties.
151
+
152
+ ### Replay and vocabulary
153
+
154
+ Successful assistant responses store a versioned, lossless-JSON replay state beside the provider and model that produced them — response-level facts plus one per-block entry per streamed block. At request time, `LlmRuntime` passes replay state only when the same adapter instance owns both routes; the adapter validates it and restores native response ids and provider signatures, degrading an unusable state to provider-neutral content instead of failing the request. pi-ai tool-call arguments are parsed objects, so the adapter parses input and re-stringifies output to the harness raw-JSON convention; pi-ai in-stream error events map to terminal `finish` chunks.
155
+
156
+ </details>
157
+
158
+ -----
159
+
160
+ <a id="further-exploration"></a>
161
+ ## Further Exploration
162
+
163
+ Read these pages when the package-level contract is not enough. They move from the service contract to the twin adapter and the shared types.
164
+
165
+ - [dsh-llm service](../llm/README.md) — the provider-neutral service this adapter registers on.
166
+ - [llm-deepseek adapter](../llm-deepseek/README.md) — the direct DeepSeek twin for the `deepseek-official` route.
167
+ - [LLM streaming subsystem](../../../docs/subsystems/llm-streaming.md) — the `StreamChunk` protocol and adapter contract.
168
+ - [llm-retry](../llm-retry/README.md) — the retry executor that applies each profile's `retryPolicy`.
169
+ - [Twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) — why the DeepSeek route ships two structurally different adapters.
170
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) — every accepted config field and its source declaration.
171
+
172
+ -----
173
+
174
+ <a id="model-experience"></a>
175
+ ## Model Experience
176
+
177
+ ### Provider request through pi-ai
178
+
179
+ #### What the model sees
180
+
181
+ The selected catalog model receives `GenerateOptions.system`, history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. When accumulated base64 image payload exceeds the route's `maxRequestImageBytes`, each offloaded image keeps its own identity and currently resolved access in replacement text. Offloaded normalized attachments are not read or transformed. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
182
+
183
+ #### Token effect
184
+
185
+ Provider tokenization governs exact input. Retained images add the stable attachment and coordinate descriptor; the offload placeholder replaces an omitted image's visual tokens. Replay metadata may let a native API reuse provider-side state.
186
+
187
+ #### KV Cache effect
188
+
189
+ Conversion preserves logical request order, while image handles and offload placeholders add model-visible text. A changed execution-world path rewrites a historical handle and can prevent reuse from that image even when attachment identity and request bytes stay stable. Changing adapter instance, provider, model, or another upstream token has the same suffix effect. Crossing the image bound replaces an earlier image with placeholder text, so reuse ends at that message until the offloaded prefix stabilizes.
190
+
191
+ ### Provider response
192
+
193
+ #### What the model sees
194
+
195
+ pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks. The adapter passes parsed tool arguments to the harness as raw JSON strings.
196
+
197
+ #### Token effect
198
+
199
+ Generated content affects later inputs only after the loop records it. pi-ai folds reasoning tokens into output usage when the provider does not report them separately, and preserves its exact `totalTokens` value unchanged.
200
+
201
+ #### KV Cache effect
202
+
203
+ Recorded response content appends to the next request and does not invalidate its earlier reusable prefix. Unrecorded transport metadata and usage accounting do not affect cache identity.
204
+
205
+ ## Known Limitations and Deferred Work
206
+
207
+ <a id="known-limitations-and-deferred-work"></a>
208
+
209
+
210
+ These limits define where the adapter stops and future work begins. They are current package constraints, not a general pi-ai comparison or a task backlog.
211
+
212
+ - **`maxRequestImageBytes` counts base64 image payload only** — text, tools, descriptors, and JSON structure ride outside the bound, so it must sit below the gateway's request-body cap with headroom. Offload is a deterministic request projection and is not recorded as a session event.
213
+ - **A sign-in lives only in the process that started it** — an authorization attempt is not durable, so reloading the page mid-login abandons it and the human starts over. Signing out is `deleteRecord` on the stored record, which forgets it locally without telling the issuer.
214
+ - **Provider-native discovery answers through this plugin's ambient context** — a route naming no credential defers to the catalog provider's own resolution, which asks for environment values (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, and each provider's own set) and for local credential files. Both questions are answered here: the credential seam is consulted before the process environment, and file existence is checked against the host process's filesystem with `~` expanded. What it cannot do is *read* a credential file's contents — a provider that parses `~/.aws/credentials` itself does so directly, outside the seam.
215
+ - **Settings can add or override routes, not remove composition routes** — the user layer merges over the composition base, so deleting a `cordis.yml`-provided provider is a composition change.
216
+ - **The layered merge has no delete for dict keys** — a `reasoningEfforts` level, `modelOverrides` entry, or `compat` field the base declares can be overridden but not removed by the user layer.
217
+ - **`headers` can carry a credential the redactor never sees** — the profile's `headers` dict is plain strings; store credentials as `apiKeyEnv` references.
218
+ - **A route's catalog never refreshes itself** — the catalog is whatever `settings.yaml` says; nothing here queries a provider for the models it serves.
219
+ - **One wire protocol per route** — a mixed-protocol catalog route cannot host a model of the other protocol; splitting the provider across two route keys is the workaround.
220
+ - **A modality declaration is not verified** — a model declaring `image` its gateway does not serve is refused by the provider after prompt admission. The durable image remains in history and the same misdeclared model can fail again; switching to a text-only model remains possible because the shared LLM runtime projects image references into stable text for that request.
221
+ - **An unauthenticated route depends on its protocol** — a route naming no credential resolves as configured-but-keyless, but pi-ai's OpenAI-compatible implementation still requires an API key or an `Authorization` header, so a keyless local server needs a placeholder credential referenced by `apiKeyEnv` or an `Authorization` entry in `headers`.
222
+ - **`GenerateOptions.stop` is unsupported** — pi-ai's common stream options cannot guarantee stop-sequence behavior across providers.
223
+ - **In-history `system` messages use pi-ai's common context conversion** — provider-specific placement follows pi-ai rather than a harness-owned wire override.
224
+ - **Provider HTTP status is unavailable** — pi-ai error events do not expose a stable HTTP status across providers.
225
+ - **Retry policy is provider-owned, not an SDK retry** — pi-ai SDK retries stay disabled so durable agent steps and `llm/retry` events own every visible attempt, and direct `ctx.llm.stream()` calls remain single-attempt.
226
+
227
+ <a id="dev-note"></a>
228
+ ### Dev Note
229
+
230
+ <details>
231
+ <summary>Working context for maintainers — click to expand</summary>
232
+
233
+ This Dev Note is non-authoritative working context: undecided directions and notes for maintainers. Shipped behavior and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
234
+
235
+ - The offered protocol set is deliberately narrower than pi-ai's full API set: Bedrock, Vertex, Azure, and Codex authenticate through flows a profile cannot completely describe with a key, an endpoint, and headers; catalog routes still reach them through their own provider, and only an explicit override is refused. Codex is sign-in-able through the authorization flow's OAuth grant.
236
+ - The `compat` switch set is pinned to pi-ai's compat types by drift gates; an upstream upgrade that adds a field, gives a further protocol a compat type, or widens a value union fails the build until someone classifies it.
237
+
238
+ </details>