@zhushanwen/pi-llm-shared 0.4.2 → 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 +5 -6
- package/src/__tests__/call.test.ts +11 -11
- package/src/call.ts +16 -14
- package/src/config.ts +5 -3
- package/src/index.ts +2 -2
- package/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-llm-shared",
|
|
3
|
-
"version": "0.
|
|
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,12 +14,11 @@
|
|
|
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.
|
|
22
|
-
"@zhushanwen/pi-file-lock": "0.
|
|
20
|
+
"@zhushanwen/pi-extension-logger": "0.4.0",
|
|
21
|
+
"@zhushanwen/pi-file-lock": "0.2.0"
|
|
23
22
|
},
|
|
24
23
|
"peerDependencies": {
|
|
25
24
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
@@ -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,
|
|
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"
|
|
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,
|
|
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({
|
|
66
|
+
expect(result).toMatchObject({ error: expect.stringContaining("network") });
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it("TC1 stopReason=error → {ok:false, error,
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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,
|
|
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"
|
|
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.reasoning
|
|
41
|
-
* minimal/low/medium/high/xhigh/max
|
|
42
|
-
*
|
|
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 →
|
|
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;
|
|
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: 提取错误文本,
|
|
89
|
+
* → {ok:false, error: 提取错误文本, stopReason}(不再当正常内容提取)
|
|
88
90
|
* 4. 提取 text → {ok:true, content}
|
|
89
|
-
* 5. throw(getApiKeyAndHeaders reject / 网络 / 超时 / 解析)→ catch → {ok:false, error:String(e)
|
|
90
|
-
* (
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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)
|
|
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 /
|
|
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
|
|
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
|
|
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,
|
|
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";
|