@zhushanwen/pi-llm-shared 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-llm-shared",
3
- "version": "0.4.1",
4
- "description": "Shared LLM invocation library for Pi extensions — model resolution (ref exact only), LLM calling (completeSimple), and config read/write with mtime caching. Shared library, not a Pi extension.",
3
+ "version": "0.5.0",
4
+ "description": "Shared LLM invocation library for Pi extensions — model resolution (ref exact only), LLM calling (completeSimple), and config read/write with mtime+size caching. Shared library, not a Pi extension.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "keywords": [
@@ -14,16 +14,15 @@
14
14
  ],
15
15
  "license": "MIT",
16
16
  "files": [
17
- "src/",
18
- "index.ts"
17
+ "src/"
19
18
  ],
20
19
  "dependencies": {
21
- "@zhushanwen/pi-extension-logger": "0.3.0",
22
- "@zhushanwen/pi-file-lock": "0.1.2"
20
+ "@zhushanwen/pi-extension-logger": "0.4.0",
21
+ "@zhushanwen/pi-file-lock": "0.2.0"
23
22
  },
24
23
  "peerDependencies": {
25
- "@earendil-works/pi-ai": "^0.84.1",
26
- "@earendil-works/pi-coding-agent": "^0.84.1"
24
+ "@earendil-works/pi-ai": "^0.84.4",
25
+ "@earendil-works/pi-coding-agent": "^0.84.4"
27
26
  },
28
27
  "peerDependenciesMeta": {
29
28
  "@earendil-works/pi-ai": {
@@ -34,8 +33,8 @@
34
33
  }
35
34
  },
36
35
  "devDependencies": {
37
- "@earendil-works/pi-ai": "^0.84.2",
38
- "@earendil-works/pi-coding-agent": "^0.84.2",
36
+ "@earendil-works/pi-ai": "^0.84.4",
37
+ "@earendil-works/pi-coding-agent": "^0.84.4",
39
38
  "@vitest/coverage-v8": "^4.1.9",
40
39
  "vitest": "^4.1.8"
41
40
  },
@@ -47,42 +47,42 @@ describe("callLLM", () => {
47
47
  expect(optionsArg).toMatchObject({ apiKey: "k", sessionId: "sess-1" });
48
48
  });
49
49
 
50
- it("TC12 auth-fail → {ok:false, recoverable:true},不调 completeSimple(narrow 不取 apiKey)", async () => {
50
+ it("TC12 auth-fail → {ok:false, error},不调 completeSimple(narrow 不取 apiKey)", async () => {
51
51
  const ctx = makeCtx({ ok: false, error: "no key" });
52
52
 
53
53
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
54
54
 
55
- expect(result).toEqual({ ok: false, error: "no key", recoverable: true });
55
+ expect(result).toEqual({ ok: false, error: "no key" });
56
56
  expect(mockComplete).not.toHaveBeenCalled();
57
57
  });
58
58
 
59
- it("TC13 completeSimple throw → {ok:false, recoverable:true, error 含错误信息}", async () => {
59
+ it("TC13 completeSimple throw → {ok:false, error 含错误信息}", async () => {
60
60
  const ctx = makeCtx({ ok: true, apiKey: "k" });
61
61
  mockComplete.mockRejectedValue(new Error("network timeout"));
62
62
 
63
63
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
64
64
 
65
65
  expect(result.ok).toBe(false);
66
- expect(result).toMatchObject({ recoverable: true, error: expect.stringContaining("network") });
66
+ expect(result).toMatchObject({ error: expect.stringContaining("network") });
67
67
  });
68
68
 
69
- it("TC1 stopReason=error → {ok:false, error, recoverable:true, stopReason:'error'}(不再 ok:true 返回错误文本)", async () => {
69
+ it("TC1 stopReason=error → {ok:false, error, stopReason:'error'}(不再 ok:true 返回错误文本)", async () => {
70
70
  const ctx = makeCtx({ ok: true, apiKey: "k" });
71
71
  // completeSimple 对错误也 resolve(带 stopReason),content 是错误文本
72
72
  mockComplete.mockResolvedValue({ stopReason: "error", content: [{ type: "text", text: "API error: 429 rate limited" }] });
73
73
 
74
74
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
75
75
 
76
- expect(result).toEqual({ ok: false, error: "API error: 429 rate limited", recoverable: true, stopReason: "error" });
76
+ expect(result).toEqual({ ok: false, error: "API error: 429 rate limited", stopReason: "error" });
77
77
  });
78
78
 
79
- it("TC2 stopReason=aborted → {ok:false, error, recoverable:true, stopReason:'aborted'}", async () => {
79
+ it("TC2 stopReason=aborted → {ok:false, error, stopReason:'aborted'}", async () => {
80
80
  const ctx = makeCtx({ ok: true, apiKey: "k" });
81
81
  mockComplete.mockResolvedValue({ stopReason: "aborted", content: [{ type: "text", text: "user aborted" }] });
82
82
 
83
83
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
84
84
 
85
- expect(result).toEqual({ ok: false, error: "user aborted", recoverable: true, stopReason: "aborted" });
85
+ expect(result).toEqual({ ok: false, error: "user aborted", stopReason: "aborted" });
86
86
  });
87
87
 
88
88
  it("TC3 stopReason=stop(正常)→ 不受 stopReason 检查影响,ok:true 提取文本", async () => {
@@ -100,7 +100,7 @@ describe("callLLM", () => {
100
100
 
101
101
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
102
102
 
103
- expect(result).toEqual({ ok: false, error: "unknown error", recoverable: true, stopReason: "error" });
103
+ expect(result).toEqual({ ok: false, error: "unknown error", stopReason: "error" });
104
104
  });
105
105
 
106
106
  it("TC13 catch 路径不设 stopReason(错误原因不可知)", async () => {
@@ -176,13 +176,13 @@ describe("callLLM", () => {
176
176
  });
177
177
 
178
178
 
179
- it("B5: getApiKeyAndHeaders reject(抛异常)→ {ok:false, recoverable:true}(归一入 catch,不向上抛)", async () => {
179
+ it("B5: getApiKeyAndHeaders reject(抛异常)→ {ok:false, error}(归一入 catch,不向上抛)", async () => {
180
180
  const getApiKeyAndHeaders = vi.fn().mockRejectedValueOnce(new Error("registry exploded"));
181
181
  const ctx = { modelRegistry: { getApiKeyAndHeaders } } as unknown as ExtensionContext;
182
182
 
183
183
  const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
184
184
 
185
- expect(result).toEqual({ ok: false, error: "registry exploded", recoverable: true });
185
+ expect(result).toEqual({ ok: false, error: "registry exploded" });
186
186
  // 凭证阶段就 reject,completeSimple 未被调用
187
187
  expect(mockComplete).not.toHaveBeenCalled();
188
188
  });
package/src/call.ts CHANGED
@@ -37,9 +37,12 @@ export interface CallLLMOptions {
37
37
  /** 透传给 SimpleStreamOptions.sessionId(provider 用于 session 缓存 / 路由)。review TF1 新增。 */
38
38
  sessionId?: string;
39
39
  /**
40
- * thinking/reasoning 级别,透传给 SimpleStreamOptions.reasoningpi 的 THINKING_ORDER SSOT:
41
- * minimal/low/medium/high/xhigh/max)。"off" 表示关闭 thinking,由本库映射为「不传 reasoning 字段」
42
- * (provider 默认行为);不传 = 同样 provider 默认。
40
+ * thinking/reasoning 级别,透传给 SimpleStreamOptions.reasoning。值域源自 pi 的
41
+ * ThinkingLevel 类型(pi-ai:ThinkingLevel = minimal/low/medium/high/xhigh/max
42
+ * ModelThinkingLevel = "off" | ThinkingLevel;pi 无 THINKING_ORDER 符号——该常量的
43
+ * SSOT 在本仓 subagent-core src/shared/model-ref.ts,值集与本类型一致)。"off" 表示
44
+ * 关闭 thinking,由本库映射为「不传 reasoning 字段」(provider 默认行为);不传 = 同样
45
+ * provider 默认。
43
46
  */
44
47
  reasoning?: ModelThinkingLevel;
45
48
  }
@@ -47,13 +50,12 @@ export interface CallLLMOptions {
47
50
  /**
48
51
  * callLLM 出参。
49
52
  * - ok:true → content 为提取并 trim 的文本
50
- * - ok:false → recoverable 表示可恢复性(C2b:当前实现统一 true,细分待未来有消费者);
51
- * stopReason 是独立透传字段(失败原因维度,不映射 recoverable),供调用方保留
53
+ * - ok:false → stopReason 是独立透传字段(失败原因维度),供调用方保留
52
54
  * error/aborted 的日志区分(如 permission classifier 的 G3 语义)。
53
55
  */
54
56
  export type CallLLMResult =
55
57
  | { ok: true; content: string }
56
- | { ok: false; error: string; recoverable: boolean; stopReason?: "error" | "aborted" };
58
+ | { ok: false; error: string; stopReason?: "error" | "aborted" };
57
59
 
58
60
  // ──────────────────────── 文本提取 ────────────────────────
59
61
 
@@ -84,10 +86,10 @@ export function extractText(resp: {
84
86
  * 保证 reject 归一为 {ok:false},调用方日志前缀一致)
85
87
  * 2. 调用:completeSimple(model, {systemPrompt, messages, tools:[]}, {apiKey, headers?, env?, signal?, maxTokens?, timeoutMs?, sessionId?})
86
88
  * 3. 检查 resp.stopReason:error/aborted(completeSimple 对错误/中止也 resolve 带 stopReason,G3)
87
- * → {ok:false, error: 提取错误文本, recoverable:true, stopReason}(不再当正常内容提取)
89
+ * → {ok:false, error: 提取错误文本, stopReason}(不再当正常内容提取)
88
90
  * 4. 提取 text → {ok:true, content}
89
- * 5. throw(getApiKeyAndHeaders reject / 网络 / 超时 / 解析)→ catch → {ok:false, error:String(e), recoverable:true}
90
- * (C2b:catch 路径不细分 recoverable,统一 true;stopReason 不设——错误原因不可知)
91
+ * 5. throw(getApiKeyAndHeaders reject / 网络 / 超时 / 解析)→ catch → {ok:false, error:String(e)}
92
+ * (stopReason 不设——错误原因不可知)
91
93
  *
92
94
  * tools 显式传 [](不塞工具)—— 本库用于标题生成等 best-effort 场景,不需要工具调用。
93
95
  */
@@ -96,13 +98,13 @@ export async function callLLM(
96
98
  opts: CallLLMOptions,
97
99
  ): Promise<CallLLMResult> {
98
100
  // 整个流程纳入 try:getApiKeyAndHeaders / completeSimple 任一 reject/throw 都归一为
99
- // {ok:false, recoverable:true},保证调用方日志前缀一致(B5:凭证注入原在 try 外,reject 时
101
+ // {ok:false},保证调用方日志前缀一致(B5:凭证注入原在 try 外,reject 时
100
102
  // callLLM 直接 reject,上游走外层 .catch 输出不一致前缀,如 [pi-rename-session] 而非 [rename-session])。
101
103
  try {
102
104
  // 1. 凭证(判别联合必须 narrow):返回 {ok:false} → 提前返回;reject(抛异常)→ 进 catch
103
105
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(opts.model);
104
106
  if (!auth.ok) {
105
- return { ok: false, error: auth.error, recoverable: true };
107
+ return { ok: false, error: auth.error };
106
108
  }
107
109
 
108
110
  // 2. 调用 completeSimple(字段名经探针⑤对齐:Context{systemPrompt?,messages,tools?},
@@ -126,13 +128,13 @@ export async function callLLM(
126
128
  };
127
129
  const resp = await completeSimple(opts.model, context, options);
128
130
  // G3/C1a:completeSimple 对 error/aborted 也 resolve(带 stopReason,不 reject)。
129
- // 归一为 ok:false + stopReason 独立透传(recoverable 统一 true,与 C2b 一致不触发细分)。
131
+ // 归一为 ok:false + stopReason 独立透传。
130
132
  if (resp.stopReason === "error" || resp.stopReason === "aborted") {
131
133
  const errorText = extractText(resp) || "unknown error";
132
- return { ok: false, error: errorText, recoverable: true, stopReason: resp.stopReason };
134
+ return { ok: false, error: errorText, stopReason: resp.stopReason };
133
135
  }
134
136
  return { ok: true, content: extractText(resp) };
135
137
  } catch (error) {
136
- return { ok: false, error: error instanceof Error ? error.message : String(error), recoverable: true };
138
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
137
139
  }
138
140
  }
package/src/config.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  * 泛型配置读写:getConfigPath + loadConfig<T> + saveConfig + mtime+size 缓存 + 原子写。
3
3
  *
4
4
  * 与 permission/config.ts 的区别:本库是泛型版(pkgName 参数化,normalize 由调用方传),
5
- * 不内置任何 schema —— rename-session / permission / scheduler consumer 各自定义 normalize。
5
+ * 不内置任何 schema —— rename-session / permission / smart-context / base-tool-enhance
6
+ * 等 consumer 各自定义 normalize。
6
7
  * 范式(mtime+size 双 key 缓存、原子写 tmp+rename、tmp 失败清理)借鉴 permission/config.ts。
7
8
  *
8
9
  * 路径解析用 pi 导出的 getAgentDir(尊重 PI_CODING_AGENT_DIR 覆盖),禁止自实现 ——
9
- * permission/config.ts:18-22 有重复自实现待 P3 清理,本库直接用 pi 导出版。
10
+ * [HISTORICAL] permission/config.ts 曾有重复自实现,P3 已清理(现全部委托本库),
11
+ * 本库直接用 pi 导出版。
10
12
  *
11
13
  * ── 热重载契约(consumer 必读) ──
12
14
  * 本库的 loadConfig 提供「读时刷新(pull-based)热重载」:每次调用 statSync 文件 mtime+size,
@@ -70,7 +72,7 @@ function clone<T>(value: T): T {
70
72
  /**
71
73
  * 加载配置,文件未变化时返回缓存(深拷贝,防调用方修改污染缓存)。
72
74
  *
73
- * @param pkgName 包名(决定文件路径 <agentDir>/config/<pkgName>.json)
75
+ * @param pkgName 包名(决定文件路径 <agentDir>/config/<pkgName>-ext-config.json)
74
76
  * @param defaults 文件缺失/坏 JSON/normalize 失败时的默认值
75
77
  * @param normalize 把 JSON.parse 的 unknown 归一化成 T(调用方负责校验 + 默认值填充)
76
78
  * @param onWarning 非致命问题(解析失败)的警告回调
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // @zhushanwen/pi-llm-shared —— 统一 public API 出口。
2
2
  // resolve: 模型解析(仅 ref 精确指定)
3
3
  // call: LLM 调用(completeSimple + 凭证 + 文本提取)
4
- // config: 泛型配置读写(mtime 缓存 + 原子写)
4
+ // config: 泛型配置读写(mtime+size 双 key 缓存 + 原子写)
5
5
  export { resolveModel, getCurrentModelId, type ModelSelector } from "./resolve.ts";
6
- export { callLLM, extractText, type CallLLMOptions, type CallLLMResult } from "./call.ts";
6
+ export { callLLM, type CallLLMOptions, type CallLLMResult } from "./call.ts";
7
7
  export { getConfigPath, loadConfig, saveConfig, clearConfigCache } from "./config.ts";
8
8
  export { migrateLegacyConfig, type MigrationResult } from "./migrate.ts";
package/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./src/index.ts";