@zhushanwen/pi-llm-shared 0.5.1 → 0.7.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-llm-shared",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
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",
@@ -17,8 +17,8 @@
17
17
  "src/"
18
18
  ],
19
19
  "dependencies": {
20
- "@zhushanwen/pi-extension-logger": "0.4.1",
21
- "@zhushanwen/pi-file-lock": "0.2.1"
20
+ "@zhushanwen/pi-extension-logger": "0.6.0",
21
+ "@zhushanwen/pi-file-lock": "0.4.0"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@earendil-works/pi-ai": "^0.84.4",
@@ -94,6 +94,39 @@ describe("callLLM", () => {
94
94
  expect(result).toEqual({ ok: true, content: "hello" });
95
95
  });
96
96
 
97
+ it("usage 透传:resp.usage 存在 → ok:true 结果透出该对象(additive,设计 §3.3 ②)", async () => {
98
+ const ctx = makeCtx({ ok: true, apiKey: "k" });
99
+ // 形态对齐 pi-ai 0.84.4 Usage(含 cost),贴近真实 provider 响应(P-usage-shape)
100
+ const usage = {
101
+ input: 10,
102
+ output: 5,
103
+ cacheRead: 2,
104
+ cacheWrite: 1,
105
+ totalTokens: 18,
106
+ cost: { input: 0.01, output: 0.02, cacheRead: 0.001, cacheWrite: 0.002, total: 0.033 },
107
+ };
108
+ mockComplete.mockResolvedValue({ stopReason: "stop", content: [{ type: "text", text: "hello" }], usage });
109
+
110
+ const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
111
+
112
+ expect(result.ok).toBe(true);
113
+ if (result.ok) {
114
+ // 透传语义:同一引用,非拷贝/重组
115
+ expect(result.usage).toBe(usage);
116
+ expect(result.content).toBe("hello");
117
+ }
118
+ });
119
+
120
+ it("usage 缺失:resp.usage 不存在 → ok:true 结果无 usage 字段(跳过落账语义由调用方处理)", async () => {
121
+ const ctx = makeCtx({ ok: true, apiKey: "k" });
122
+ mockComplete.mockResolvedValue({ stopReason: "stop", content: [{ type: "text", text: "hello" }] });
123
+
124
+ const result = await callLLM(ctx, { model: makeModel(), systemPrompt: "s", messages: [] });
125
+
126
+ expect(result.ok).toBe(true);
127
+ expect(result).not.toHaveProperty("usage");
128
+ });
129
+
97
130
  it("stopReason=error 且 content 无 text → error 回落 'unknown error'", async () => {
98
131
  const ctx = makeCtx({ ok: true, apiKey: "k" });
99
132
  mockComplete.mockResolvedValue({ stopReason: "error", content: [] });
@@ -21,7 +21,7 @@ vi.mock("@zhushanwen/pi-extension-logger", () => ({
21
21
  // 用 vi.mock 包装 readFileSync/renameSync/statSync/unlinkSync(默认走 actual,
22
22
  // 个别 test override),其他 fs 操作(writeFileSync/existsSync/mkdirSync...)原样
23
23
  // 透传 actual。statSync/unlinkSync 供 U4 logger.warn 留痕用例 override。
24
- // 注意:proper-lockfile(withFileLockSync 内部)走 graceful-fs,不受本 mock 影响。
24
+ // 注意:withFileLockSync 内部为自实现 mkdir-lock(lock-core.ts,零依赖直用 node:fs,非 proper-lockfile);本 mock 默认透传 actual,锁行为不受影响。
25
25
  vi.mock("node:fs", async (importOriginal) => {
26
26
  const actual = await importOriginal() as typeof import("node:fs");
27
27
  return {
@@ -50,7 +50,7 @@ afterEach(() => {
50
50
  vi.mocked(fs.statSync).mockClear();
51
51
  vi.mocked(fs.unlinkSync).mockClear();
52
52
  loggerMock.warn.mockClear();
53
- rmSync(dir, { recursive: true, force: true });
53
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
54
54
  vi.unstubAllEnvs();
55
55
  });
56
56
 
@@ -10,7 +10,7 @@ describe("migrateLegacyConfig", () => {
10
10
  dir = mkdtempSync(join(tmpdir(), "pi-migrate-test-"));
11
11
  });
12
12
  afterEach(() => {
13
- rmSync(dir, { recursive: true, force: true });
13
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
14
14
  });
15
15
 
16
16
  it("旧路径不存在 → noop(migrated: false,不动文件系统)", () => {
package/src/call.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  Context as LlmContext,
19
19
  SimpleStreamOptions,
20
20
  } from "@earendil-works/pi-ai/compat";
21
- import type { Api, Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
21
+ import type { Api, Message, Model, ModelThinkingLevel, Usage } from "@earendil-works/pi-ai";
22
22
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
23
23
 
24
24
  // ──────────────────────── 类型 ────────────────────────
@@ -49,12 +49,14 @@ export interface CallLLMOptions {
49
49
 
50
50
  /**
51
51
  * callLLM 出参。
52
- * - ok:true → content 为提取并 trim 的文本
52
+ * - ok:true → content 为提取并 trim 的文本;usage 为 completeSimple 响应的 resp.usage 透传
53
+ * (可选,存在才带——usage 整体缺失时不带字段,「跳过落账」语义由调用方处理,如
54
+ * rename-session 的 appendEntry 存在性守卫;permission classifier 等既有调用方不消费,零影响)。
53
55
  * - ok:false → stopReason 是独立透传字段(失败原因维度),供调用方保留
54
56
  * error/aborted 的日志区分(如 permission classifier 的 G3 语义)。
55
57
  */
56
58
  export type CallLLMResult =
57
- | { ok: true; content: string }
59
+ | { ok: true; content: string; usage?: Usage }
58
60
  | { ok: false; error: string; stopReason?: "error" | "aborted" };
59
61
 
60
62
  // ──────────────────────── 文本提取 ────────────────────────
@@ -87,7 +89,8 @@ export function extractText(resp: {
87
89
  * 2. 调用:completeSimple(model, {systemPrompt, messages, tools:[]}, {apiKey, headers?, env?, signal?, maxTokens?, timeoutMs?, sessionId?})
88
90
  * 3. 检查 resp.stopReason:error/aborted(completeSimple 对错误/中止也 resolve 带 stopReason,G3)
89
91
  * → {ok:false, error: 提取错误文本, stopReason}(不再当正常内容提取)
90
- * 4. 提取 text → {ok:true, content}
92
+ * 4. 提取 text → {ok:true, content};resp.usage 存在则一并透传(条件 spread,缺失时不带字段——
93
+ * AssistantMessage.usage 类型必填但运行时 provider 可能不回,P-usage-shape 的降级分支)
91
94
  * 5. throw(getApiKeyAndHeaders reject / 网络 / 超时 / 解析)→ catch → {ok:false, error:String(e)}
92
95
  * (stopReason 不设——错误原因不可知)
93
96
  *
@@ -133,7 +136,12 @@ export async function callLLM(
133
136
  const errorText = extractText(resp) || "unknown error";
134
137
  return { ok: false, error: errorText, stopReason: resp.stopReason };
135
138
  }
136
- return { ok: true, content: extractText(resp) };
139
+ return {
140
+ ok: true,
141
+ content: extractText(resp),
142
+ // additive(设计 §3.3 ②):透传 resp.usage,存在才带;缺失时整体不带字段
143
+ ...(resp.usage ? { usage: resp.usage } : {}),
144
+ };
137
145
  } catch (error) {
138
146
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
139
147
  }
package/src/index.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  export { resolveModel, getCurrentModelId, type ModelSelector } from "./resolve.ts";
6
6
  export { callLLM, type CallLLMOptions, type CallLLMResult } from "./call.ts";
7
7
  export { getConfigPath, loadConfig, saveConfig, clearConfigCache } from "./config.ts";
8
- export { migrateLegacyConfig, type MigrationResult } from "./migrate.ts";
8
+ export { migrateLegacyConfig } from "./migrate.ts";