@zhushanwen/pi-permission 1.4.9 → 1.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-permission",
3
- "version": "1.4.9",
3
+ "version": "1.5.0",
4
4
  "description": "Pi permission extension — four permission modes (yolo/auto/approve/strict) with three-layer pipeline (AST + rules + AI classifier).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -52,7 +52,7 @@
52
52
  "web-tree-sitter": "^0.26.11",
53
53
  "@zhushanwen/pi-ext-guards": "0.4.2",
54
54
  "@zhushanwen/pi-extension-logger": "0.6.1",
55
- "@zhushanwen/pi-llm-shared": "0.10.1"
55
+ "@zhushanwen/pi-llm-shared": "0.11.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@earendil-works/pi-ai": "^0.84.4",
@@ -282,4 +282,27 @@ describe("CT5: callLLM opts 构造(model/messages/timeoutMs/signal)", () =>
282
282
  const opts = getLastOpts();
283
283
  expect(opts?.signal).toBe(ac.signal);
284
284
  });
285
+
286
+ it("配置 thinkingLevel 不被模型支持 → warn 留痕 + reasoning 不传(不静默降级)", async () => {
287
+ // FIXED_MODEL_OBJ reasoning:false → supportedLevels = ["off"],配置 "high" 判定落空;
288
+ // changeset pi-permission-thinking-level-datadriven 承诺 warn 留痕(同族 rename-session 对齐)
289
+ const onLog = vi.fn();
290
+ const { spy, getLastOpts } = capturingCallLLM(okResult('{"outcome":"allow","risk_level":"low","reasoning":"x","confidence":0.9}'));
291
+ const classifier = createClassifier(makeDeps({ callLLM: spy, onLog }));
292
+ const r = await classifier.classifyRisk(CTX, { ...CONFIG, thinkingLevel: "high" });
293
+ expect(r.outcome).toBe("allow");
294
+ expect(onLog).toHaveBeenCalledWith(
295
+ expect.stringContaining('configured thinkingLevel "high" is not supported by test-co/test-model; calling without a thinking level'),
296
+ );
297
+ expect(getLastOpts()?.reasoning).toBeUndefined();
298
+ });
299
+
300
+ it("配置 thinkingLevel 被模型支持 → 命中透传,无 warn", async () => {
301
+ const onLog = vi.fn();
302
+ const { spy, getLastOpts } = capturingCallLLM(okResult('{"outcome":"allow","risk_level":"low","reasoning":"x","confidence":0.9}'));
303
+ const classifier = createClassifier(makeDeps({ callLLM: spy, onLog }));
304
+ await classifier.classifyRisk(CTX, { ...CONFIG, thinkingLevel: "off" });
305
+ expect(onLog).not.toHaveBeenCalledWith(expect.stringContaining("not supported"));
306
+ expect(getLastOpts()?.reasoning).toBe("off");
307
+ });
285
308
  });
@@ -17,6 +17,7 @@
17
17
  * - fail-closed:timeout / 抛错 / 解析失败 / 无可用模型 → 一律 ask
18
18
  */
19
19
 
20
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
20
21
  import type { Api, Message, Model } from "@earendil-works/pi-ai";
21
22
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
22
23
  import type { CallLLMOptions, CallLLMResult } from "@zhushanwen/pi-llm-shared";
@@ -133,15 +134,25 @@ function buildCallOptions(
133
134
  config: ClassifierConfig,
134
135
  timeoutMs: number | undefined,
135
136
  signal: AbortSignal | undefined,
137
+ onLog?: (msg: string) => void,
136
138
  ): CallLLMOptions {
139
+ // 档位按「该模型自己的 supportedLevels」判定(pi-ai 数据驱动,本层不自持词表);
140
+ // 不支持则按「不传档位」处理,不静默换成别的档(clamp),并 warn 留痕
141
+ // (changeset pi-permission-thinking-level-datadriven 的承诺,与 rename-session 同族对齐)。
142
+ // llm-shared 会把 "off" 映射为不传。
143
+ const reasoning = getSupportedThinkingLevels(model).find((l) => l === config.thinkingLevel);
144
+ if (config.thinkingLevel !== undefined && config.thinkingLevel !== "" && reasoning === undefined) {
145
+ onLog?.(
146
+ `[pi-permission] classifier: configured thinkingLevel "${config.thinkingLevel}" is not supported by ${model.provider}/${model.id}; calling without a thinking level`,
147
+ );
148
+ }
137
149
  return {
138
150
  model,
139
151
  systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
140
152
  messages: buildMessages(ctx),
141
153
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
142
154
  ...(signal !== undefined ? { signal } : {}),
143
- // thinkingLevel 直接透传(含 "off");llm-shared 内部会把 "off" 映射为不传 reasoning(provider 默认)
144
- reasoning: config.thinkingLevel,
155
+ reasoning,
145
156
  };
146
157
  }
147
158
 
@@ -223,7 +234,7 @@ export function createClassifier(deps: ClassifierDeps): {
223
234
 
224
235
  // 2. 构造 CallLLMOptions
225
236
  const timeoutMs = config.timeout > 0 ? config.timeout * MILLIS_PER_SECOND : undefined;
226
- const callPromise = callLLM(buildCallOptions(model, ctx, config, timeoutMs, signal));
237
+ const callPromise = callLLM(buildCallOptions(model, ctx, config, timeoutMs, signal, onLog));
227
238
 
228
239
  // 3. 等待 callLLM + 外层超时/中止兜底(provider 不支持 timeoutMs/signal 时防永挂)
229
240
  let settled: SettledRace;
package/src/config.ts CHANGED
@@ -22,7 +22,7 @@ import { isRecord, toErrorMessage } from "@zhushanwen/pi-ext-guards";
22
22
  import {
23
23
  clearConfigCache,
24
24
  getConfigPath as getLlmConfigPath,
25
- isThinkingLevel,
25
+ normalizeThinkingLevel,
26
26
  loadConfig,
27
27
  saveConfig as saveLlmConfig,
28
28
  } from "@zhushanwen/pi-llm-shared";
@@ -59,10 +59,8 @@ function normalizeClassifierConfig(raw: unknown): ClassifierConfig {
59
59
  if (record.model !== undefined && !(typeof record.model === "string" && record.model.length > 0)) {
60
60
  logger.warn("Ignoring invalid classifier.model (expected string 'auto' or 'provider/model-id'), using default auto");
61
61
  }
62
- // thinkingLevel 校验:合法值为 'off'|'minimal'|'low'|'medium'|'high'|'xhigh'|'max'
63
- const thinkingLevel = isThinkingLevel(record.thinkingLevel)
64
- ? record.thinkingLevel
65
- : DEFAULT_CLASSIFIER_CONFIG.thinkingLevel;
62
+ // thinkingLevel:非空字符串即采信(合法档位由写入侧 UI 与 pi 判定;本层不再自持词表)
63
+ const thinkingLevel = normalizeThinkingLevel(record.thinkingLevel) ?? DEFAULT_CLASSIFIER_CONFIG.thinkingLevel;
66
64
  return {
67
65
  enabled: record.enabled !== false,
68
66
  model: typeof record.model === "string" && record.model.length > 0 ? record.model : DEFAULT_CLASSIFIER_CONFIG.model,
package/src/types.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * 对应 slice plan 的 DM1-DM5 数据模型。
6
6
  */
7
7
 
8
- import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
9
8
 
10
9
  // ──────────────────────── DM1: PermissionMode ────────────────────────
11
10
 
@@ -108,11 +107,11 @@ export interface ClassifierConfig {
108
107
  /** 高风险是否强制 deny(不问人:high+allow 时直接拒绝) */
109
108
  autoDenyHighRisk: boolean;
110
109
  /**
111
- * 标题生成 LLM 的 thinking 级别(pi 的 ModelThinkingLevel,THINKING_ORDER SSOT)。
110
+ * 标题生成 LLM 的 thinking 级别(字符串透传;合法档位按模型 supportedLevels 在调用点判定)。
112
111
  * 默认 "off":不传 pi-ai reasoning(provider 默认行为);
113
112
  * "minimal"~"max" 透传给 SimpleStreamOptions.reasoning(provider 不支持时静默忽略)。
114
113
  */
115
- thinkingLevel: ModelThinkingLevel;
114
+ thinkingLevel: string;
116
115
  }
117
116
 
118
117
  // ──────────────────────── DM5: PermissionConfig ────────────────────────