@foolsecret/pi-prompt 0.4.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/CHANGELOG.md +86 -0
- package/LICENSE +667 -0
- package/README.md +121 -0
- package/extensions/index.ts +12 -0
- package/package.json +57 -0
- package/prompts/prompt-review.skill.md +28 -0
- package/src/auto.ts +146 -0
- package/src/calibration.ts +205 -0
- package/src/command.ts +54 -0
- package/src/config.ts +280 -0
- package/src/format.ts +11 -0
- package/src/modes.ts +113 -0
- package/src/prompt-extension.ts +623 -0
- package/src/prompts.ts +197 -0
- package/src/request.ts +25 -0
- package/src/stats.ts +430 -0
- package/src/time.ts +40 -0
- package/src/ui.ts +156 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# pi-prompt
|
|
2
|
+
|
|
3
|
+
**Pi 的输出精简插件:少说废话,省真金白银。**
|
|
4
|
+
|
|
5
|
+
一句话:把「每次回复都啰嗦一大段、输出 token 悄悄烧钱」的痛,换成「三条正交轴随手调,auto 档按你的真实价格自动选最省钱的那一档」。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 为什么需要它
|
|
10
|
+
|
|
11
|
+
输出 token 单价远高于输入(DeepSeek 输出 4~8 元/百万 vs 输入 miss 1 元/百万),
|
|
12
|
+
而多数回复里的寒暄、复述、铺垫都可以不要。pi-prompt 用一段**字节稳定的追加
|
|
13
|
+
提示**,让模型按你选的力度输出——不改变模型能力,只砍掉冗余表达。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 开箱你能得到什么
|
|
18
|
+
|
|
19
|
+
- **三条正交轴,随手调**
|
|
20
|
+
- `show`:输出风格力度(`normal` / `lite` / `full` / `ultra` / `auto`)
|
|
21
|
+
- `write`:代码写量(`minimal` / `normal` / `complete`)
|
|
22
|
+
- `do`:行为力度(`direct` / `normal` / `deep`)
|
|
23
|
+
- **`auto` 档:按成本自动选**。用成本函数
|
|
24
|
+
`净收益 = µ × 缩减率 × 输出价 − 注入成本 × 输入价` 选最优档,
|
|
25
|
+
并随校准样本自动收敛;小任务倾向不注入,大任务才上强度。
|
|
26
|
+
- **价格交给你自己管**。auto 与成本估算都读 pi-pricer 的共享价表
|
|
27
|
+
(`~/.pi/model-pricing.json`)——包括你自定义的峰谷、促销、节假日规则。
|
|
28
|
+
没装 pi-pricer 也能用(回退内置近似价,并会提示你装)。
|
|
29
|
+
- **渐进式自进化校准**。只采自然样本,零额外花费;想要更快收敛可主动跑
|
|
30
|
+
`/prompt check --calibrate`(3 轮不注入的对照探针)。
|
|
31
|
+
- **台账与自检**。`/prompt usage` 给你按天×模型聚合 + 会话明细的 token/金额表,
|
|
32
|
+
并标注节省估算;`/prompt check` 看台账完整性、校准状态与当前价格来源。
|
|
33
|
+
- **恒定注入语义**。注入文案按档位编号、字节稳定,不破坏 prompt 前缀缓存命中。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 快速开始(30 秒)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. 安装
|
|
41
|
+
pi extension add @foolsecret/pi-prompt
|
|
42
|
+
|
|
43
|
+
# 2. (推荐)安装 pi-pricer 并维护你的真实价格
|
|
44
|
+
pi extension add @foolsecret/pi-pricer
|
|
45
|
+
/price # 打开价格面板,改成你实际付费的价
|
|
46
|
+
|
|
47
|
+
# 3. 打开三轴设置抽屉
|
|
48
|
+
/prompt config # 会话三轴 + 默认三轴,改完即生效
|
|
49
|
+
|
|
50
|
+
# 4. 看看花了多少、省了多少
|
|
51
|
+
/prompt usage
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 命令速查
|
|
57
|
+
|
|
58
|
+
| 命令 | 干嘛的 |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `/prompt` | 无参 = 全局开关(关停/恢复注入) |
|
|
61
|
+
| `/prompt config` | 三轴设置抽屉(会话档 + 默认档,改完即持久化) |
|
|
62
|
+
| `/prompt status` | 当前三轴状态 |
|
|
63
|
+
| `/prompt usage` | token/金额台账(按天×模型 + 会话明细 + 节省估算) |
|
|
64
|
+
| `/prompt check` | 自检:台账完整性 / 校准状态 / 价格来源;`--calibrate` 跑探针 |
|
|
65
|
+
| `/prompt-review` | 调用 `prompt-review` 技能审计上一条回复的冗余 |
|
|
66
|
+
|
|
67
|
+
状态栏恒常显示 `PROMPT <show> · <write> · <do>`,`show=auto` 显示本轮实际解析档。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 配置
|
|
72
|
+
|
|
73
|
+
配置文件 `~/.pi/pi-prompt.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"defaultShow": "auto",
|
|
78
|
+
"defaultWrite": "normal",
|
|
79
|
+
"defaultDo": "normal",
|
|
80
|
+
"peakUpgrade": true,
|
|
81
|
+
"maxTokensCap": null,
|
|
82
|
+
"autoSample": false
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
环境变量(优先级高于配置):`PI_PROMPT_SHOW` / `PI_PROMPT_WRITE` / `PI_PROMPT_DO` /
|
|
87
|
+
`PI_PROMPT_PEAK` / `PI_PROMPT_MAX_TOKENS` / `PI_PROMPT_AUTO_SAMPLE`。
|
|
88
|
+
|
|
89
|
+
历史数据兼容:旧档名 `off` 读取时自动归一为 `normal`,不影响既有台账。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 价格来源
|
|
94
|
+
|
|
95
|
+
pi-prompt 自己**不维护价表**。成本估算与 auto 选档都从 pi-pricer 取价:
|
|
96
|
+
|
|
97
|
+
| 情况 | 行为 |
|
|
98
|
+
|---|---|
|
|
99
|
+
| 装了 pi-pricer | 用你在 `~/.pi/model-pricing.json` 配的真实价格与峰谷规则 |
|
|
100
|
+
| 没装 | 回退内置近似价(会在首次 `/prompt usage` 提示你安装) |
|
|
101
|
+
| 装了但解析失败 | 回退兜底价,并在 `/prompt check` 给出失败原因 |
|
|
102
|
+
|
|
103
|
+
所以想让账目对得上、让 auto 判断准,推荐同时装
|
|
104
|
+
[`@foolsecret/pi-pricer`](https://www.npmjs.com/package/@foolsecret/pi-pricer)。
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 扩展开发者接入
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { resolvePricing } from "@foolsecret/pi-pricer/pricing";
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
pi-prompt 自身是 `@foolsecret/pi-pricer` 的**可选 peer 依赖**:
|
|
115
|
+
未安装时功能完整(回退内置兜底价),不会因缺依赖而加载失败。
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
AGPL-3.0-only。详见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-prompt 扩展入口(薄入口):主逻辑在 src/prompt-extension.ts。
|
|
3
|
+
* manifest 显式指向本文件;加载即装配 PromptExtension 到 pi 生命周期。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PromptExtension } from "../src/prompt-extension.ts";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
/** pi 扩展工厂:实例化并挂载输出精简插件 */
|
|
10
|
+
export default function (pi: ExtensionAPI): void {
|
|
11
|
+
new PromptExtension().mount(pi);
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@foolsecret/pi-prompt",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "为 Pi Agent 打造的输出精简提示插件 — 多档力度压缩输出 token 省钱 | A prompt-verbosity plugin for Pi Agent — tiered output compression to save tokens & cost",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"prompt",
|
|
9
|
+
"verbosity",
|
|
10
|
+
"concise",
|
|
11
|
+
"token",
|
|
12
|
+
"savings"
|
|
13
|
+
],
|
|
14
|
+
"license": "AGPL-3.0-only",
|
|
15
|
+
"author": "Titor-Z",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Titor-Z/pi-prompt.git"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src/",
|
|
25
|
+
"extensions/",
|
|
26
|
+
"prompts/",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test --test-force-exit test/*.test.mjs",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
35
|
+
},
|
|
36
|
+
"pi": {
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./extensions/index.ts"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*",
|
|
44
|
+
"@foolsecret/pi-pricer": "^0.12.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@foolsecret/pi-pricer": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
53
|
+
"@types/node": "^22.20.2",
|
|
54
|
+
"jiti": "^2.4.2",
|
|
55
|
+
"typescript": "^7.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prompt-review
|
|
3
|
+
description: Audit the assistant's previous reply for unnecessary verbosity and, when the latest reply is verbose, rewrite it once in a concise form. Use after turning on pi-prompt review mode or whenever the user asks to trim a reply.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# prompt-review
|
|
7
|
+
|
|
8
|
+
审计上一条回复:找出明显冗余,并在必要时用精炼版重写一次。
|
|
9
|
+
|
|
10
|
+
## 判定标准(符合任一再重写)
|
|
11
|
+
|
|
12
|
+
- 头/尾有与问题无关的寒暄、铺垫或"下面我将…"式预告
|
|
13
|
+
- 复述了用户已经给出的事实或上下文
|
|
14
|
+
- 有重复的结论、冗余的过渡词、客套句(如"随时问我 / 希望对你有帮助")
|
|
15
|
+
- 散文解释长度超过代码本身(代码场景)
|
|
16
|
+
|
|
17
|
+
## 重写规则
|
|
18
|
+
|
|
19
|
+
- 只输出被审回复的核心结论:结论先行、列表优先、每句删到不可再删
|
|
20
|
+
- 保留用户明确要求的信息、错误处理细节、路径/报错原文
|
|
21
|
+
- 不新增信息、不改变建议、不改结论
|
|
22
|
+
- 重写后追加一行审计结果:`[prompt-review] 原 N 行/约 M 词 → 新 N 行/约 M 词`(粗略估算即可)
|
|
23
|
+
- 原回复已足够精炼时,只输出一行结果,不再重写
|
|
24
|
+
|
|
25
|
+
## 边界
|
|
26
|
+
|
|
27
|
+
- 永远不删除:错误处理、安全提示、用户点名要求的信息
|
|
28
|
+
- 审计对象是"怎么说",不是"写什么"——不要建议删功能/删代码
|
package/src/auto.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto 选档器(纯函数核心 + 类封装)。
|
|
3
|
+
*
|
|
4
|
+
* 数学内核(与用户讨论的函数关系,见 AGENTS.md 讨论记录):
|
|
5
|
+
* net(mode) = estMu × r(mode) × P_输出 − inj(mode) × P_输入(缓存状态)
|
|
6
|
+
* 净收益最高的档 = argmax;在"平局阈值"内选更保守的档(normal<lite<full<ultra),
|
|
7
|
+
* 阈值随台账样本量收缩 —— 数据多了自然收敛到纯数学最优(自进化)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RuntimeShowMode } from "./modes.ts";
|
|
11
|
+
import { INJECTION_TOKENS_BY_SHOW, type TaskBin } from "./stats.ts";
|
|
12
|
+
import { beijingDate, isDeepSeekPeakHours } from "./time.ts";
|
|
13
|
+
|
|
14
|
+
// isDeepSeekPeakHours / beijingDate 已下沉到 time.ts(供 stats 兜底价复用,
|
|
15
|
+
// 避免 auto ↔ stats 循环依赖);此处重导出以保持既有调用点与测试不变。
|
|
16
|
+
export { beijingDate, isDeepSeekPeakHours };
|
|
17
|
+
|
|
18
|
+
/** 选档所需全部数学输入(纯函数参数,方便单测) */
|
|
19
|
+
export interface TierMathInput {
|
|
20
|
+
/** 该任务不注入时的预期输出 token(µ) */
|
|
21
|
+
estMu: number;
|
|
22
|
+
/** 输出单价(¥/百万 token) */
|
|
23
|
+
outputPrice: number;
|
|
24
|
+
/** 输入未命中单价(¥/百万 token) */
|
|
25
|
+
inputMissPrice: number;
|
|
26
|
+
/** 输入缓存命中单价(¥/百万 token) */
|
|
27
|
+
inputHitPrice: number;
|
|
28
|
+
/** 本回合前缀是否大概率已缓存 */
|
|
29
|
+
cached: boolean;
|
|
30
|
+
/** 各档缩减率先验或校准拟合值 */
|
|
31
|
+
reduction: (mode: RuntimeShowMode) => number;
|
|
32
|
+
/** 各档注入 token(成本项) */
|
|
33
|
+
injectionTokens: (mode: RuntimeShowMode) => number;
|
|
34
|
+
/** 同类样本量(平局阈值收缩依据) */
|
|
35
|
+
sampleCount: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 平局阈值的基准比例(相对最优净收益):净收益差距小于该比例视为"差不多" */
|
|
39
|
+
export const BASE_TIE_FRACTION: number = 0.05;
|
|
40
|
+
|
|
41
|
+
/** 校准样本量 → 平局阈值收缩(数据多了逼近纯数学最优) */
|
|
42
|
+
export function tieFraction(sampleCount: number): number {
|
|
43
|
+
if (sampleCount >= 800) return 0.005;
|
|
44
|
+
if (sampleCount >= 200) return 0.02;
|
|
45
|
+
return BASE_TIE_FRACTION;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 保守度排序:normal < lite < full < ultra(数值越小越保守) */
|
|
49
|
+
const CONSERVATISM: readonly RuntimeShowMode[] = ["normal", "lite", "full", "ultra"];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 核心选档(纯函数):
|
|
53
|
+
* 1. 对 4 档算 net(mode);
|
|
54
|
+
* 2. 取净收益最高的档;
|
|
55
|
+
* 3. 在 {最优 × (1 − 平局比例)} 范围内选最保守的一档。
|
|
56
|
+
* 注意平局阈值必须是相对比例(绝对 ¥ 在微元量级上会把一切判成平局)。
|
|
57
|
+
*/
|
|
58
|
+
export function chooseAuto(input: TierMathInput): RuntimeShowMode {
|
|
59
|
+
const priceIn = input.cached ? input.inputHitPrice : input.inputMissPrice;
|
|
60
|
+
const nets = new Map<RuntimeShowMode, number>();
|
|
61
|
+
let bestNet = Number.NEGATIVE_INFINITY;
|
|
62
|
+
for (const mode of CONSERVATISM) {
|
|
63
|
+
const net =
|
|
64
|
+
(input.estMu * input.reduction(mode) * input.outputPrice -
|
|
65
|
+
input.injectionTokens(mode) * priceIn) /
|
|
66
|
+
1_000_000;
|
|
67
|
+
nets.set(mode, net);
|
|
68
|
+
if (net > bestNet) bestNet = net;
|
|
69
|
+
}
|
|
70
|
+
const tie = Math.abs(bestNet) * tieFraction(input.sampleCount);
|
|
71
|
+
for (const mode of CONSERVATISM) {
|
|
72
|
+
// 升序遍历:第一个进入平局带的档 = 最保守的接近最优
|
|
73
|
+
if ((nets.get(mode) ?? Number.NEGATIVE_INFINITY) >= bestNet - tie) return mode;
|
|
74
|
+
}
|
|
75
|
+
return "normal";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 请求规模 → 预期输出 µ 的粗糙分桶(先验;可被校准的 normal 基线中位数覆盖) */
|
|
79
|
+
export function estimateMuFromInput(inputTokens: number): number {
|
|
80
|
+
if (inputTokens <= 80) return 30;
|
|
81
|
+
if (inputTokens <= 400) return 120;
|
|
82
|
+
if (inputTokens <= 1500) return 350;
|
|
83
|
+
if (inputTokens <= 5000) return 800;
|
|
84
|
+
return 1500;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 请求规模 → 任务分桶(校准分组键) */
|
|
88
|
+
export function taskBinForInput(inputTokens: number): TaskBin {
|
|
89
|
+
if (inputTokens <= 80) return "tiny";
|
|
90
|
+
if (inputTokens <= 400) return "short";
|
|
91
|
+
if (inputTokens <= 1500) return "mid";
|
|
92
|
+
if (inputTokens <= 5000) return "long";
|
|
93
|
+
return "xlong";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 价格表读取接口(各厂商单价来源,供注入测试) */
|
|
97
|
+
export interface ProviderPricing {
|
|
98
|
+
output: number;
|
|
99
|
+
miss: number;
|
|
100
|
+
hit: number;
|
|
101
|
+
/** 命中峰谷(由价格源判定:pi-pricer 按用户 schedule,兜底按 DeepSeek 固定时段) */
|
|
102
|
+
isPeak?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* auto 档选择器类:把价格源/校准缩减率/样本量封装成一次选档调用。
|
|
107
|
+
* 依赖注入:价格、缩减率、样本量全部可替换,测试用假数据。
|
|
108
|
+
*/
|
|
109
|
+
export class AutoTierChooser {
|
|
110
|
+
constructor(
|
|
111
|
+
private readonly priceFor: (provider: string, model: string, ts: number) => ProviderPricing,
|
|
112
|
+
private readonly reductionFor: (provider: string, bin: TaskBin, mode: RuntimeShowMode) => number,
|
|
113
|
+
private readonly sampleCountFor: (provider: string, bin: TaskBin) => number,
|
|
114
|
+
) {}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 给定厂商/模型/任务桶/µ 估计/缓存状态/时刻,选出注入档。
|
|
118
|
+
* 峰谷不再由调用方按硬编码时段推算,而是取价格源返回的 isPeak —— 这样用户在
|
|
119
|
+
* pi-pricer 配置里改了峰时段,auto 立即跟随(兜底价格源才退回 DeepSeek 固定时段)。
|
|
120
|
+
* peakUpgrade=false 时忽略 isPeak(关闭高峰升档,保持基线定价)。
|
|
121
|
+
*/
|
|
122
|
+
choose(
|
|
123
|
+
provider: string,
|
|
124
|
+
model: string,
|
|
125
|
+
bin: TaskBin,
|
|
126
|
+
estMu: number,
|
|
127
|
+
cached: boolean,
|
|
128
|
+
ts: number,
|
|
129
|
+
peakUpgrade: boolean = true,
|
|
130
|
+
): RuntimeShowMode {
|
|
131
|
+
const prices = this.priceFor(provider, model, ts);
|
|
132
|
+
const sampleCount = this.sampleCountFor(provider, bin);
|
|
133
|
+
const peak = peakUpgrade && prices.isPeak === true;
|
|
134
|
+
const outputPrice = peak ? prices.output * 2 : prices.output;
|
|
135
|
+
return chooseAuto({
|
|
136
|
+
estMu,
|
|
137
|
+
outputPrice,
|
|
138
|
+
inputMissPrice: prices.miss,
|
|
139
|
+
inputHitPrice: prices.hit,
|
|
140
|
+
cached,
|
|
141
|
+
reduction: (mode) => this.reductionFor(provider, bin, mode),
|
|
142
|
+
injectionTokens: (mode) => INJECTION_TOKENS_BY_SHOW[mode],
|
|
143
|
+
sampleCount,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 校准管理器(渐进式自进化):用本地台账拟合各厂商×任务桶的
|
|
3
|
+
* show 轴缩减率 r(mode) 与 normal 基线 µ,写 ~/.pi/pi-prompt-calibration.json。
|
|
4
|
+
*
|
|
5
|
+
* 诚实口径:真实验证缩减率需要"同类任务不注入"的对照组(counterfactual)。
|
|
6
|
+
* 数据来源三路:
|
|
7
|
+
* 1. 自然样本 —— 用户手动跑 normal(不注入)的回合(对照组)与各档回合;
|
|
8
|
+
* 2. 用户主动校准 —— /prompt check --calibrate 才跑成对探针(eg. N 轮);
|
|
9
|
+
* 3. 样本不足时不动系数(惰性更新),auto 继续用先验(REDUCTION_BY_SHOW)。
|
|
10
|
+
* 自进化 = "数据够才改",不对用户偷偷产生 API 花费。
|
|
11
|
+
*
|
|
12
|
+
* v0.2.2:show 轴旧档名 off → normal;历史校准文件里的 off 键读取时按同语义
|
|
13
|
+
* 合并(对照组 = normal 基线),写盘一律用新键名。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { normalizeRuntimeShow, type RuntimeShowMode } from "./modes.ts";
|
|
20
|
+
import { getActiveResolver, type PricingResolver, type UsageRecord } from "./stats.ts";
|
|
21
|
+
|
|
22
|
+
/** 校准文件路径(~/.pi/pi-prompt-calibration.json) */
|
|
23
|
+
export const CALIBRATION_PATH: string = join(homedir(), ".pi", "pi-prompt-calibration.json");
|
|
24
|
+
|
|
25
|
+
/** 触发拟合的最低样本量:对照组 ≥ 此数、目标档 ≥ 此数才写系数 */
|
|
26
|
+
export const MIN_CONTROL_SAMPLES: number = 5;
|
|
27
|
+
export const MIN_MODE_SAMPLES: number = 3;
|
|
28
|
+
|
|
29
|
+
/** 单档拟合结果 */
|
|
30
|
+
export interface ModeFit {
|
|
31
|
+
r: number;
|
|
32
|
+
n: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 单任务桶拟合:normal 基线 µ + 各档 r */
|
|
36
|
+
export interface BinFit {
|
|
37
|
+
normal?: { n: number };
|
|
38
|
+
/** normal 基线 µ(对照组中位数),auto 的 µ 估计覆盖用 */
|
|
39
|
+
mu?: number;
|
|
40
|
+
lite?: ModeFit;
|
|
41
|
+
full?: ModeFit;
|
|
42
|
+
ultra?: ModeFit;
|
|
43
|
+
/** v0.2.2 前的旧文件名:off 基线样本数(读取兼容,写盘并入 normal) */
|
|
44
|
+
off?: { n: number };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 对照组样本数(normal 优先;兼容旧文件 off 键) */
|
|
48
|
+
function controlCount(fit: BinFit | undefined): number {
|
|
49
|
+
return fit?.normal?.n ?? fit?.off?.n ?? 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 单厂商拟合 */
|
|
53
|
+
export interface ProviderFit {
|
|
54
|
+
byBin: Record<string, BinFit>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 校准文件结构 */
|
|
58
|
+
export interface CalibrationFile {
|
|
59
|
+
version: 1;
|
|
60
|
+
updatedAt: number;
|
|
61
|
+
byProvider: Record<string, ProviderFit>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 空校准文件(无数据时的默认态) */
|
|
65
|
+
function emptyCalibration(): CalibrationFile {
|
|
66
|
+
return { version: 1, updatedAt: 0, byProvider: {} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 奇数个取中位近似:升序排列取中值(n≥1 才调用) */
|
|
70
|
+
function median(values: number[]): number {
|
|
71
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
72
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 校准管理器类:读档 / 查询拟合值 / 用台账重拟合。
|
|
77
|
+
* 样本不足查询返回 null —— 调用方(auto)回落到先验。
|
|
78
|
+
*/
|
|
79
|
+
export class CalibrationManager {
|
|
80
|
+
constructor(private readonly filePath: string = CALIBRATION_PATH) {}
|
|
81
|
+
|
|
82
|
+
/** 惰性读档;不存在/损坏 → 空校准(无副作用写盘) */
|
|
83
|
+
load(): CalibrationFile {
|
|
84
|
+
try {
|
|
85
|
+
const raw = readFileSync(this.filePath, "utf8").replace(/^\uFEFF/, "");
|
|
86
|
+
const parsed = JSON.parse(raw) as Partial<CalibrationFile>;
|
|
87
|
+
if (parsed?.version !== 1 || typeof parsed.byProvider !== "object") return emptyCalibration();
|
|
88
|
+
return { version: 1, updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0, byProvider: parsed.byProvider };
|
|
89
|
+
} catch {
|
|
90
|
+
return emptyCalibration();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 冷读磁盘上的校准文件(每次调用都读盘,供 /prompt check 展示最新) */
|
|
95
|
+
refresh(): CalibrationFile {
|
|
96
|
+
return this.load();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 某厂商×桶的 normal 基线 µ(拟合存在才返回,否则 null) */
|
|
100
|
+
muFor(provider: string, bin: string): number | null {
|
|
101
|
+
const mu = this.load().byProvider[provider]?.byBin[bin]?.mu;
|
|
102
|
+
return typeof mu === "number" ? mu : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 某厂商×桶的缩减率拟合值(样本足才返回,否则 null → 用先验;normal 无 r 语义) */
|
|
106
|
+
reductionFor(provider: string, bin: string, mode: RuntimeShowMode): number | null {
|
|
107
|
+
const binFit = this.load().byProvider[provider]?.byBin[bin];
|
|
108
|
+
if (!binFit || mode === "normal") return null;
|
|
109
|
+
const fit = binFit[mode];
|
|
110
|
+
if (!fit || typeof fit.r !== "number" || fit.n < MIN_MODE_SAMPLES) return null;
|
|
111
|
+
return Math.min(Math.max(fit.r, 0), 0.95);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 某厂商×桶已收集的样本量(auto 平局阈值收缩依据;含兼容旧 off 键) */
|
|
115
|
+
sampleCountFor(provider: string, bin: string): number {
|
|
116
|
+
const binFit = this.load().byProvider[provider]?.byBin[bin];
|
|
117
|
+
if (!binFit) return 0;
|
|
118
|
+
const count = controlCount(binFit) + (binFit.lite?.n ?? 0) + (binFit.full?.n ?? 0) + (binFit.ultra?.n ?? 0);
|
|
119
|
+
return count;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 用台账重拟合:按 (厂商, 桶, show) 分组,对照组 = show=normal 的输出中位数
|
|
124
|
+
* (历史记录里的 off 已由台账读取归一为 normal),
|
|
125
|
+
* r(mode) = 1 − median(mode)/median(normal),截断到 [0, 0.95]。
|
|
126
|
+
* 只有样本足够的桶才更新;有变化才写盘返回 changed=true。
|
|
127
|
+
*/
|
|
128
|
+
recalibrate(records: readonly UsageRecord[]): { changed: boolean; file: CalibrationFile } {
|
|
129
|
+
const file = this.load();
|
|
130
|
+
let changed = false;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
|
|
133
|
+
// 分组:provider → bin → mode → outputs[](record.show 统一归一,防手工写 off)
|
|
134
|
+
const groups = new Map<string, Map<string, Map<RuntimeShowMode, number[]>>>();
|
|
135
|
+
for (const record of records) {
|
|
136
|
+
let byBin = groups.get(record.provider);
|
|
137
|
+
if (!byBin) {
|
|
138
|
+
byBin = new Map();
|
|
139
|
+
groups.set(record.provider, byBin);
|
|
140
|
+
}
|
|
141
|
+
let byMode = byBin.get(record.taskBin);
|
|
142
|
+
if (!byMode) {
|
|
143
|
+
byMode = new Map();
|
|
144
|
+
byBin.set(record.taskBin, byMode);
|
|
145
|
+
}
|
|
146
|
+
const mode = normalizeRuntimeShow(record.show) ?? "normal";
|
|
147
|
+
const outputs = byMode.get(mode) ?? [];
|
|
148
|
+
outputs.push(record.output);
|
|
149
|
+
byMode.set(mode, outputs);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [provider, byBin] of groups) {
|
|
153
|
+
const providerFit = file.byProvider[provider] ?? { byBin: {} };
|
|
154
|
+
for (const [bin, byMode] of byBin) {
|
|
155
|
+
const controlOutputs = byMode.get("normal") ?? [];
|
|
156
|
+
if (controlOutputs.length < MIN_CONTROL_SAMPLES) continue;
|
|
157
|
+
const controlMedian = median(controlOutputs);
|
|
158
|
+
const binFit = providerFit.byBin[bin] ?? {};
|
|
159
|
+
const mu = typeof binFit.mu === "number" ? binFit.mu : controlMedian;
|
|
160
|
+
// 与旧值比对,有变化才标记(避免无意义落盘);旧 off 键由 normal 取代
|
|
161
|
+
const updated = { ...binFit, normal: { n: controlOutputs.length }, mu, off: undefined } as BinFit;
|
|
162
|
+
for (const mode of ["lite", "full", "ultra"] as const) {
|
|
163
|
+
const outputs = byMode.get(mode) ?? [];
|
|
164
|
+
if (outputs.length < MIN_MODE_SAMPLES) continue;
|
|
165
|
+
const r = Math.min(Math.max(1 - median(outputs) / controlMedian, 0), 0.95);
|
|
166
|
+
updated[mode] = { r, n: outputs.length };
|
|
167
|
+
}
|
|
168
|
+
const before = JSON.stringify(providerFit.byBin[bin] ?? {});
|
|
169
|
+
providerFit.byBin[bin] = updated;
|
|
170
|
+
if (JSON.stringify(updated) !== before) changed = true;
|
|
171
|
+
}
|
|
172
|
+
file.byProvider[provider] = providerFit;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (changed) {
|
|
176
|
+
file.updatedAt = now;
|
|
177
|
+
try {
|
|
178
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
179
|
+
writeFileSync(this.filePath, JSON.stringify(file, null, 2), "utf8");
|
|
180
|
+
} catch {
|
|
181
|
+
// 写校准失败不阻断:本次拟合作废,下次再试
|
|
182
|
+
return { changed: false, file };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { changed, file };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 厂商单价(calibration/auto 共用)。
|
|
191
|
+
*
|
|
192
|
+
* **价格来源**:默认走 stats.ts 的 activeResolver —— 装了 pi-pricer 就是用户
|
|
193
|
+
* 在 ~/.pi/model-pricing.json 里配置的权威价(含峰谷);未装则退回内置近似价。
|
|
194
|
+
* 峰谷不再由调用方按硬编码时段推算,而是随价格一并返回(isPeak),
|
|
195
|
+
* 这样用户改峰时段即影响 auto 选档。
|
|
196
|
+
*/
|
|
197
|
+
export function pricingFor(
|
|
198
|
+
provider: string,
|
|
199
|
+
model: string,
|
|
200
|
+
ts: number,
|
|
201
|
+
resolver: PricingResolver = getActiveResolver(),
|
|
202
|
+
): { output: number; miss: number; hit: number; isPeak: boolean } {
|
|
203
|
+
const price = resolver(model, provider, ts);
|
|
204
|
+
return { output: price.output, miss: price.inputMiss, hit: price.inputHit, isPeak: price.isPeak === true };
|
|
205
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /prompt 命令解析器(纯函数,方便单测)。
|
|
3
|
+
*
|
|
4
|
+
* 语法(v0.2.1 起,设置统一进 config 抽屉,不再提供 show/write/do/default 子命令):
|
|
5
|
+
* /prompt 无参: 全局 master 开关(全轴 normal ↔ 默认)
|
|
6
|
+
* /prompt config 打开三轴设置抽屉(复刻 /settings:会话档 + 默认档)
|
|
7
|
+
* /prompt status 显示三轴现状与台账估算
|
|
8
|
+
* /prompt usage 显示 token/金额台账(DeepSeek 风格表)
|
|
9
|
+
* /prompt check [--calibrate] 健康自检;--calibrate 才跑成对探针并重拟合
|
|
10
|
+
* 其余输入一律判非法(提示用 /prompt config)。
|
|
11
|
+
* review 不再作为档位,请用 /prompt-review 技能
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { normalizeShowMode } from "./modes.ts";
|
|
15
|
+
|
|
16
|
+
/** 三轴名(config 抽屉行 id 也按此约束) */
|
|
17
|
+
export type AxisName = "show" | "write" | "do";
|
|
18
|
+
|
|
19
|
+
/** 解析结果的判别联合 */
|
|
20
|
+
export type PromptCommand =
|
|
21
|
+
| { type: "toggle" }
|
|
22
|
+
| { type: "config" }
|
|
23
|
+
| { type: "status" }
|
|
24
|
+
| { type: "usage" }
|
|
25
|
+
| { type: "check"; calibrate: boolean }
|
|
26
|
+
| { type: "invalid"; arg: string };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 解析 /prompt 参数文本。
|
|
30
|
+
* 首个 token 决定分支;斜杠前缀(/full 之类)容忍掉。
|
|
31
|
+
*/
|
|
32
|
+
export function parsePromptCommand(args: string): PromptCommand {
|
|
33
|
+
const parts = String(args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
34
|
+
const primary = (parts[0] ?? "").replace(/^\/+/, "").toLowerCase();
|
|
35
|
+
|
|
36
|
+
switch (primary) {
|
|
37
|
+
case "":
|
|
38
|
+
return { type: "toggle" };
|
|
39
|
+
case "config":
|
|
40
|
+
return { type: "config" };
|
|
41
|
+
case "status":
|
|
42
|
+
return { type: "status" };
|
|
43
|
+
case "usage":
|
|
44
|
+
return { type: "usage" };
|
|
45
|
+
case "check":
|
|
46
|
+
return { type: "check", calibrate: parts.includes("--calibrate") };
|
|
47
|
+
default:
|
|
48
|
+
// v0.1/v0.2 的裸档位(off/lite/full/ultra/auto)与 show|write|do 子命令
|
|
49
|
+
// 已被 config 抽屉取代:给出明确引导而非静默失败
|
|
50
|
+
return normalizeShowMode(primary) !== null
|
|
51
|
+
? { type: "invalid", arg: "请用 /prompt config 打开抽屉设置档位" }
|
|
52
|
+
: { type: "invalid", arg: parts[0] ?? "" };
|
|
53
|
+
}
|
|
54
|
+
}
|