pi-model-manager 0.1.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/LICENSE +661 -0
- package/NOTICE +8 -0
- package/README.en.md +206 -0
- package/README.md +210 -0
- package/atomic-write.ts +100 -0
- package/builtin-model-catalog.ts +92 -0
- package/claude-code-compat.ts +56 -0
- package/common.ts +66 -0
- package/compat-settings.ts +25 -0
- package/config-value-reference.ts +51 -0
- package/configuration-persistence.ts +72 -0
- package/header-profile-mutations.ts +46 -0
- package/index.ts +82 -0
- package/local-proxy-service.ts +313 -0
- package/model-mutations.ts +210 -0
- package/models-json-manager.ts +238 -0
- package/models-json-mutations.ts +90 -0
- package/models-json-sync.ts +386 -0
- package/openai-responses-payload.ts +80 -0
- package/openai-service-tier.ts +36 -0
- package/package.json +74 -0
- package/presets/builtin-client-headers.ts +23 -0
- package/presets/client-headers.ts +126 -0
- package/presets/providers.ts +80 -0
- package/presets/thinking.ts +81 -0
- package/provider-registrar.ts +281 -0
- package/request-pipeline.ts +88 -0
- package/rescue.ts +69 -0
- package/runtime-base-url.ts +54 -0
- package/state-cache.ts +55 -0
- package/state-document.ts +569 -0
- package/state-metadata-store.ts +455 -0
- package/state-store.ts +238 -0
- package/tui/dashboard.ts +376 -0
- package/tui/editor-model.ts +339 -0
- package/tui/editor-provider.ts +219 -0
- package/tui/header-profiles-panel.ts +307 -0
- package/tui/model-list-fetch.ts +259 -0
- package/tui/model-picker.ts +266 -0
- package/tui/models-json-panel.ts +269 -0
- package/tui/persistent-menu.ts +349 -0
- package/tui/ui-helpers.ts +289 -0
- package/types.ts +165 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# pi-model-manager
|
|
2
|
+
|
|
3
|
+
English · [简体中文](./README.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/earendil-works/pi)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/pi-model-manager)
|
|
8
|
+
|
|
9
|
+
A TUI model and provider manager for [Pi](https://github.com/earendil-works/pi). It keeps Pi's native `models.json` as the single source of truth for model configuration while adding provider/model editing, client-header identities, proxy routing, and protocol compatibility controls.
|
|
10
|
+
|
|
11
|
+
> The current stable version is `0.1.0` and requires Pi `>=0.82.0`.
|
|
12
|
+
|
|
13
|
+
## Interface preview
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
<details>
|
|
18
|
+
<summary>View all four full-size screenshots</summary>
|
|
19
|
+
|
|
20
|
+
### Provider setup
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
### Model discovery
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Provider and model overview
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### Model capabilities
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Create, edit, and delete providers and models from the `/model-manager` TUI.
|
|
40
|
+
- Supports `openai-completions`, `openai-responses`, `anthropic-messages`, and `google-generative-ai`.
|
|
41
|
+
- Fetch model IDs from compatible upstream APIs or enter them manually.
|
|
42
|
+
- Configure context window, maximum output, vision input, and reasoning support.
|
|
43
|
+
- Choose Anthropic Adaptive Thinking or Legacy Thinking.
|
|
44
|
+
- Enable `service_tier=priority` (Fast mode) per OpenAI Responses model.
|
|
45
|
+
- Route each provider directly or through its own HTTP(S) proxy.
|
|
46
|
+
- Use recommended, disabled, Claude Code, Codex, or custom client-header profiles.
|
|
47
|
+
- Reference API keys as literals, `$ENV_VAR` / `${ENV_VAR}`, or Pi `!command` values.
|
|
48
|
+
- Persist configuration atomically and re-register managed providers after saving.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
### Install from GitHub (recommended for now)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pi install git:github.com/Qihuanxishini/pi-model-manager
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Try it for the current run without adding it to Pi's package settings:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pi -e git:github.com/Qihuanxishini/pi-model-manager
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Update Git-installed extensions with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pi update --extensions
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Install from npm
|
|
71
|
+
|
|
72
|
+
`pi-model-manager` is published as a public npm package:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pi install npm:pi-model-manager
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
1. Start the Pi TUI.
|
|
81
|
+
2. Run:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/model-manager
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. Manage providers from the dashboard:
|
|
88
|
+
|
|
89
|
+
| Key | Action |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `Enter` | Open the selected provider and manage its models |
|
|
92
|
+
| `n` | Create a provider and its first model |
|
|
93
|
+
| `d` | Delete the selected provider |
|
|
94
|
+
| `h` | Manage reusable header profiles |
|
|
95
|
+
| `Esc` | Go back or exit |
|
|
96
|
+
|
|
97
|
+
4. In an editor, use the arrow keys to select a field and press `Enter` to edit it. Toggle supported fields with `←` / `→`, then press `Ctrl+S` to save.
|
|
98
|
+
|
|
99
|
+
Saving updates and enables the model, but it does not force the current session to switch models.
|
|
100
|
+
|
|
101
|
+
## Provider and model configuration
|
|
102
|
+
|
|
103
|
+
Each provider can define:
|
|
104
|
+
|
|
105
|
+
- API protocol and base URL
|
|
106
|
+
- API key and authorization-header behavior
|
|
107
|
+
- Client-header identity
|
|
108
|
+
- Provider-specific HTTP(S) proxy
|
|
109
|
+
- One or more models
|
|
110
|
+
|
|
111
|
+
When adding a model, the extension attempts to fetch the upstream model list with a 10-second timeout. You can still enter a model ID manually if discovery fails.
|
|
112
|
+
|
|
113
|
+
Model capabilities include:
|
|
114
|
+
|
|
115
|
+
- Display name
|
|
116
|
+
- Text or text-and-image input
|
|
117
|
+
- Reasoning support
|
|
118
|
+
- Anthropic Adaptive/Legacy Thinking
|
|
119
|
+
- OpenAI Responses Fast mode
|
|
120
|
+
- Context window and maximum output tokens
|
|
121
|
+
|
|
122
|
+
## Client-header profiles
|
|
123
|
+
|
|
124
|
+
| Profile | Behavior |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| Recommended | Claude Code for Anthropic Messages, Codex for OpenAI Completions/Responses, and no identity headers for other protocols |
|
|
127
|
+
| Disabled | Adds no extension-managed client identity headers |
|
|
128
|
+
| Claude Code | Uses the built-in Claude Code compatibility headers and adds required Anthropic request metadata |
|
|
129
|
+
| Codex | Uses the built-in Codex TUI compatibility headers |
|
|
130
|
+
| Custom | Uses a reusable JSON header set created in the header-profile panel |
|
|
131
|
+
|
|
132
|
+
The current built-in values were derived from real client requests with authentication fields removed:
|
|
133
|
+
|
|
134
|
+
- Claude Code `2.1.219`
|
|
135
|
+
- Codex TUI `0.145.0`
|
|
136
|
+
|
|
137
|
+
These headers only help API gateways that require a recognized client identity; they do not replace an API key. The public repository and npm package contain **no request-capture tooling, user captures, authentication headers, or machine-local state**. Disable identity headers or create a custom profile if a built-in profile does not fit your endpoint.
|
|
138
|
+
|
|
139
|
+
Custom profiles reject authentication-related sensitive headers. Put authentication material in the provider's API-key setting instead.
|
|
140
|
+
|
|
141
|
+
## Configuration files
|
|
142
|
+
|
|
143
|
+
| Path | Purpose |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `~/.pi/agent/models.json` | Native Pi provider and model definitions; the single source of truth for model configuration |
|
|
146
|
+
| `~/.pi/agent/extensions/pi-model-manager/state.json` | Extension metadata such as header choices, custom profiles, proxy switches, and Fast mode |
|
|
147
|
+
|
|
148
|
+
The extension preserves native `models.json` fields that are outside the TUI's editing scope whenever possible. Avoid modifying the same configuration file from multiple processes at once.
|
|
149
|
+
|
|
150
|
+
## Secrets and security
|
|
151
|
+
|
|
152
|
+
Pi extensions run with the current user's permissions and have full system access. Review the source before installing any third-party extension.
|
|
153
|
+
|
|
154
|
+
Prefer environment-variable or command references over plaintext secrets in `models.json`:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
$OPENAI_API_KEY
|
|
158
|
+
${ANTHROPIC_API_KEY}
|
|
159
|
+
!your-secret-command
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Additional considerations:
|
|
163
|
+
|
|
164
|
+
- Custom client headers are not a credential store.
|
|
165
|
+
- Enabling a provider proxy routes that provider's requests through the configured proxy URL.
|
|
166
|
+
- Model discovery sends a network request to the configured upstream endpoint.
|
|
167
|
+
- The repository ignores `state.json`, runtime logs, request captures, and other machine-specific files.
|
|
168
|
+
|
|
169
|
+
## Compatibility
|
|
170
|
+
|
|
171
|
+
| Component | Requirement |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `@earendil-works/pi-coding-agent` | `>=0.82.0` |
|
|
174
|
+
| `@earendil-works/pi-tui` | `>=0.75.0` |
|
|
175
|
+
| Runtime mode | `/model-manager` requires the Pi TUI |
|
|
176
|
+
|
|
177
|
+
The current TUI copy is Simplified Chinese.
|
|
178
|
+
|
|
179
|
+
## Local development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git clone https://github.com/Qihuanxishini/pi-model-manager.git
|
|
183
|
+
cd pi-model-manager
|
|
184
|
+
npm install
|
|
185
|
+
pi -e .
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Pi loads the TypeScript entry point directly; no separate build step is required. Do not commit `state.json`, `bootstrap-meta.json`, `node_modules`, or request-capture data.
|
|
189
|
+
|
|
190
|
+
Inspect the public package contents with:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npm pack --dry-run
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Reporting issues
|
|
197
|
+
|
|
198
|
+
Open a reproducible report in [GitHub Issues](https://github.com/Qihuanxishini/pi-model-manager/issues). Remove API keys, authentication headers, proxy credentials, and private endpoints before sharing configuration or logs.
|
|
199
|
+
|
|
200
|
+
## Acknowledgements
|
|
201
|
+
|
|
202
|
+
Thanks to the [LINUX DO](https://linux.do/) community for discussion, sharing, and feedback.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
Official releases from `v0.1.0` onward are licensed under the [GNU Affero General Public License v3.0 only](./LICENSE). Modified distributions must provide the corresponding source, remain under AGPL-3.0, and identify their changes; they must not represent themselves as official releases. See [NOTICE](./NOTICE).
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# pi-model-manager
|
|
2
|
+
|
|
3
|
+
[English](./README.en.md) · 简体中文
|
|
4
|
+
|
|
5
|
+
[](https://github.com/earendil-works/pi)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://www.npmjs.com/package/pi-model-manager)
|
|
8
|
+
|
|
9
|
+
一个面向 [Pi](https://github.com/earendil-works/pi) 的 TUI 模型与接入管理扩展。它以 Pi 原生 `models.json` 为模型配置的唯一权威来源,并提供接入/模型编辑、请求头身份、代理路由和协议兼容配置。
|
|
10
|
+
|
|
11
|
+
> 当前稳定版为 `0.1.0`,要求 Pi `>=0.82.0`。
|
|
12
|
+
|
|
13
|
+
## 界面预览
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
<details>
|
|
18
|
+
<summary>查看四张完整截图</summary>
|
|
19
|
+
|
|
20
|
+
### 接入配置
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
### 模型发现
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### 接入与模型总览
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### 模型能力
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
## 功能
|
|
38
|
+
|
|
39
|
+
- 在 `/model-manager` TUI 中新增、编辑和删除接入与模型。
|
|
40
|
+
- 原生支持 `openai-completions`、`openai-responses`、`anthropic-messages` 和 `google-generative-ai`。
|
|
41
|
+
- 从兼容上游拉取模型 ID,也可手动填写模型。
|
|
42
|
+
- 配置上下文窗口、最大输出、视觉输入和 Thinking。
|
|
43
|
+
- 支持 Anthropic Adaptive Thinking 与 Legacy Thinking。
|
|
44
|
+
- 可为 OpenAI Responses 模型启用 `service_tier=priority`(Fast mode)。
|
|
45
|
+
- 为每个接入单独配置直连或 HTTP(S) 代理。
|
|
46
|
+
- 提供自动推荐、禁用、Claude Code、Codex 和自定义请求头模式。
|
|
47
|
+
- API key 支持字面值、`$ENV_VAR` / `${ENV_VAR}` 和 Pi 的 `!command` 引用。
|
|
48
|
+
- 原子写入配置,并在保存后重新注册受管理的 Provider,无需手工编辑多处分散状态。
|
|
49
|
+
|
|
50
|
+
## 安装
|
|
51
|
+
|
|
52
|
+
### 从 GitHub 安装(当前推荐)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pi install git:github.com/Qihuanxishini/pi-model-manager
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
也可以先临时试用,不写入 Pi 的包配置:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pi -e git:github.com/Qihuanxishini/pi-model-manager
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
更新 Git 安装的扩展:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pi update --extensions
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 从 npm 安装
|
|
71
|
+
|
|
72
|
+
`pi-model-manager` 已发布为公开 npm 包:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pi install npm:pi-model-manager
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 快速开始
|
|
79
|
+
|
|
80
|
+
1. 启动 Pi TUI。
|
|
81
|
+
2. 执行:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/model-manager
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. 在主面板中管理接入:
|
|
88
|
+
|
|
89
|
+
| 按键 | 操作 |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `Enter` | 进入所选接入并管理模型 |
|
|
92
|
+
| `n` | 新建接入及其第一个模型 |
|
|
93
|
+
| `d` | 删除所选接入 |
|
|
94
|
+
| `h` | 管理可复用请求头 |
|
|
95
|
+
| `Esc` | 返回或退出 |
|
|
96
|
+
|
|
97
|
+
4. 在编辑器中使用方向键选择字段,按 `Enter` 编辑;支持切换的字段可用 `←` / `→` 调整;按 `Ctrl+S` 保存。
|
|
98
|
+
|
|
99
|
+
保存会更新并启用模型,但不会强制切换当前会话正在使用的模型。
|
|
100
|
+
|
|
101
|
+
## 配置模型
|
|
102
|
+
|
|
103
|
+
### 接入配置
|
|
104
|
+
|
|
105
|
+
每个接入可以配置:
|
|
106
|
+
|
|
107
|
+
- API 协议与 Base URL
|
|
108
|
+
- API key 与认证头行为
|
|
109
|
+
- 请求头身份
|
|
110
|
+
- 接入级 HTTP(S) 代理
|
|
111
|
+
- 一个或多个模型
|
|
112
|
+
|
|
113
|
+
新建模型时,扩展会尝试在 10 秒内从上游读取模型列表;失败后仍可手动输入模型 ID。
|
|
114
|
+
|
|
115
|
+
### 模型能力
|
|
116
|
+
|
|
117
|
+
模型编辑器支持:
|
|
118
|
+
|
|
119
|
+
- 显示名称
|
|
120
|
+
- 文本或文本+图像输入
|
|
121
|
+
- Thinking 开关
|
|
122
|
+
- Anthropic Adaptive/Legacy Thinking 协议
|
|
123
|
+
- OpenAI Responses Fast mode
|
|
124
|
+
- Context window 与最大输出 token
|
|
125
|
+
|
|
126
|
+
## 请求头模式
|
|
127
|
+
|
|
128
|
+
| 模式 | 行为 |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| 自动推荐 | Anthropic Messages 使用 Claude Code;OpenAI Completions/Responses 使用 Codex;其他协议不附加身份头 |
|
|
131
|
+
| 不添加 | 不添加扩展管理的客户端身份请求头 |
|
|
132
|
+
| Claude Code | 使用内置 Claude Code 兼容请求头,并为 Anthropic 请求补充必要的兼容 metadata |
|
|
133
|
+
| Codex | 使用内置 Codex TUI 兼容请求头 |
|
|
134
|
+
| 自定义 | 使用在请求头面板中创建的可复用 JSON 请求头集合 |
|
|
135
|
+
|
|
136
|
+
当前内置值来自真实客户端请求并已移除认证信息:
|
|
137
|
+
|
|
138
|
+
- Claude Code `2.1.219`
|
|
139
|
+
- Codex TUI `0.145.0`
|
|
140
|
+
|
|
141
|
+
这些值只用于兼容需要识别客户端身份的 API 中转,不代替 API key。公开仓库和 npm 包**不包含请求捕获工具、用户抓包、认证头或本机状态**。如果内置值不适合你的服务,请关闭身份头或创建自定义请求头。
|
|
142
|
+
|
|
143
|
+
自定义请求头会拒绝认证类敏感字段;认证信息应放在接入的 API key 配置中。
|
|
144
|
+
|
|
145
|
+
## 配置文件
|
|
146
|
+
|
|
147
|
+
| 路径 | 用途 |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `~/.pi/agent/models.json` | Pi 原生接入与模型定义;模型配置的唯一权威来源 |
|
|
150
|
+
| `~/.pi/agent/extensions/pi-model-manager/state.json` | 请求头选择、自定义请求头、代理开关和 Fast mode 等扩展私有元数据 |
|
|
151
|
+
|
|
152
|
+
扩展会尽量保留 `models.json` 中不由 TUI 编辑的原生字段。不要同时让多个进程修改同一配置文件。
|
|
153
|
+
|
|
154
|
+
## 密钥与安全
|
|
155
|
+
|
|
156
|
+
Pi 扩展以当前用户权限运行并拥有完整系统访问能力。安装任何第三方扩展前都应审阅源码。
|
|
157
|
+
|
|
158
|
+
推荐通过环境变量或命令引用 API key,避免把密钥明文写入 `models.json`:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
$OPENAI_API_KEY
|
|
162
|
+
${ANTHROPIC_API_KEY}
|
|
163
|
+
!your-secret-command
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
其他注意事项:
|
|
167
|
+
|
|
168
|
+
- 自定义请求头不是保存认证凭据的位置。
|
|
169
|
+
- 启用接入代理后,该接入的请求会经过你填写的代理地址。
|
|
170
|
+
- 拉取模型列表会向所配置的上游地址发起网络请求。
|
|
171
|
+
- 仓库忽略 `state.json`、运行日志、请求捕获数据和其他机器专属文件。
|
|
172
|
+
|
|
173
|
+
## 兼容性
|
|
174
|
+
|
|
175
|
+
| 组件 | 要求 |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `@earendil-works/pi-coding-agent` | `>=0.82.0` |
|
|
178
|
+
| `@earendil-works/pi-tui` | `>=0.75.0` |
|
|
179
|
+
| 运行模式 | `/model-manager` 需要 Pi TUI |
|
|
180
|
+
|
|
181
|
+
当前 TUI 文案为简体中文。
|
|
182
|
+
|
|
183
|
+
## 本地开发
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
git clone https://github.com/Qihuanxishini/pi-model-manager.git
|
|
187
|
+
cd pi-model-manager
|
|
188
|
+
npm install
|
|
189
|
+
pi -e .
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
扩展由 Pi 直接加载 TypeScript 入口,不需要单独构建步骤。开发时请勿提交 `state.json`、`bootstrap-meta.json`、`node_modules` 或任何请求捕获数据。
|
|
193
|
+
|
|
194
|
+
验证公开包内容:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm pack --dry-run
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 问题反馈
|
|
201
|
+
|
|
202
|
+
请通过 [GitHub Issues](https://github.com/Qihuanxishini/pi-model-manager/issues) 提交可复现的问题。报告配置问题时,请删除 API key、认证头、代理凭据和私有 endpoint。
|
|
203
|
+
|
|
204
|
+
## 致谢
|
|
205
|
+
|
|
206
|
+
感谢 [LINUX DO](https://linux.do/) 社区的讨论、分享和反馈。
|
|
207
|
+
|
|
208
|
+
## 许可证
|
|
209
|
+
|
|
210
|
+
`v0.1.0` 及后续官方版本采用 [GNU Affero General Public License v3.0 only](./LICENSE)。分发修改版本时必须公开对应源码、继续使用 AGPL-3.0,并明确标注改动;修改版不得冒充官方发行。详见 [NOTICE](./NOTICE)。
|
package/atomic-write.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Windows 友好的配置文件持久化写入。
|
|
2
|
+
//
|
|
3
|
+
// 目标文件可能被正在运行的 pi、编辑器、杀软或索引器短暂占用。
|
|
4
|
+
// 直接 rename(tmp, target) 在 Windows 上会留下 tmp 且正式配置不变;这里统一
|
|
5
|
+
// 处理重试、降级覆盖与失败时的可恢复临时文件提示。
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { copyFile, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
const REPLACE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400, 800] as const;
|
|
12
|
+
const TRANSIENT_REPLACE_ERROR_CODES = new Set(["EACCES", "EPERM", "EBUSY", "ENOTEMPTY"]);
|
|
13
|
+
|
|
14
|
+
function sleep(ms: number): Promise<void> {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
19
|
+
return error instanceof Error && "code" in error
|
|
20
|
+
? String((error as NodeJS.ErrnoException).code)
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isTransientReplaceError(error: unknown): boolean {
|
|
25
|
+
const code = getErrorCode(error);
|
|
26
|
+
return code !== undefined && TRANSIENT_REPLACE_ERROR_CODES.has(code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatError(error: unknown): string {
|
|
30
|
+
return error instanceof Error ? error.message : String(error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function removeTempFile(path: string): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
await unlink(path);
|
|
36
|
+
} catch {
|
|
37
|
+
// 清理失败不应掩盖真实保存结果;失败场景会把 tmp 路径报告给用户用于恢复。
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function renameWithRetry(sourcePath: string, targetPath: string): Promise<void> {
|
|
42
|
+
let lastError: unknown;
|
|
43
|
+
for (let attempt = 0; attempt <= REPLACE_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
44
|
+
try {
|
|
45
|
+
await rename(sourcePath, targetPath);
|
|
46
|
+
return;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
lastError = error;
|
|
49
|
+
if (!isTransientReplaceError(error) || attempt === REPLACE_RETRY_DELAYS_MS.length) break;
|
|
50
|
+
await sleep(REPLACE_RETRY_DELAYS_MS[attempt]!);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw lastError;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function copyOverTargetWithRetry(sourcePath: string, targetPath: string): Promise<void> {
|
|
57
|
+
let lastError: unknown;
|
|
58
|
+
for (let attempt = 0; attempt <= REPLACE_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
59
|
+
try {
|
|
60
|
+
await copyFile(sourcePath, targetPath);
|
|
61
|
+
await removeTempFile(sourcePath);
|
|
62
|
+
return;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
lastError = error;
|
|
65
|
+
if (!isTransientReplaceError(error) || attempt === REPLACE_RETRY_DELAYS_MS.length) break;
|
|
66
|
+
await sleep(REPLACE_RETRY_DELAYS_MS[attempt]!);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function replaceTargetFile(sourcePath: string, targetPath: string): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await renameWithRetry(sourcePath, targetPath);
|
|
75
|
+
} catch (renameError) {
|
|
76
|
+
if (process.platform !== "win32" || !isTransientReplaceError(renameError)) throw renameError;
|
|
77
|
+
// [喵喵喵]: Windows 上 rename 覆盖会被短暂文件锁拦截;重试后降级 copyFile,避免配置保存停留在 tmp (2026-06-21)
|
|
78
|
+
try {
|
|
79
|
+
await copyOverTargetWithRetry(sourcePath, targetPath);
|
|
80
|
+
} catch (copyError) {
|
|
81
|
+
throw new Error(`rename 失败:${formatError(renameError)};copy 覆盖也失败:${formatError(copyError)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function atomicWriteText(targetPath: string, content: string): Promise<void> {
|
|
87
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
88
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
89
|
+
let tempHasCompleteContent = false;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await writeFile(tempPath, content, "utf8");
|
|
93
|
+
tempHasCompleteContent = true;
|
|
94
|
+
await replaceTargetFile(tempPath, targetPath);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!tempHasCompleteContent) await removeTempFile(tempPath);
|
|
97
|
+
const recoveryNote = tempHasCompleteContent ? `;完整内容已保留在临时文件:${tempPath}` : "";
|
|
98
|
+
throw new Error(`写入 ${targetPath} 失败${recoveryNote}:${formatError(error)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Pi 内置模型 catalog 边界。
|
|
2
|
+
//
|
|
3
|
+
// [喵喵喵]: Pi 0.80.8 移除 AuthStorage 且目录运行时异步化,必须隔离读取内置目录 (2026-07-17)
|
|
4
|
+
// 仅为读取 Pi 内置目录创建运行时,不读取用户 auth.json/models.json,
|
|
5
|
+
// 也不允许目录读取触发模型网络请求。
|
|
6
|
+
|
|
7
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ApiKind } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
interface BuiltinProviderDefaults {
|
|
11
|
+
api?: ApiKind;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BuiltinCatalogModel {
|
|
16
|
+
provider: string;
|
|
17
|
+
api: string;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SUPPORTED_API_KINDS = new Set<ApiKind>([
|
|
22
|
+
"openai-completions",
|
|
23
|
+
"openai-responses",
|
|
24
|
+
"anthropic-messages",
|
|
25
|
+
"google-generative-ai",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const emptyCredentialStore = {
|
|
29
|
+
async read(): Promise<undefined> {
|
|
30
|
+
return undefined;
|
|
31
|
+
},
|
|
32
|
+
async list(): Promise<readonly []> {
|
|
33
|
+
return [];
|
|
34
|
+
},
|
|
35
|
+
async modify(): Promise<undefined> {
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
async delete(): Promise<void> {
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let builtinModelsPromise: Promise<readonly BuiltinCatalogModel[]> | undefined;
|
|
44
|
+
let builtinProviderIdsPromise: Promise<ReadonlySet<string>> | undefined;
|
|
45
|
+
|
|
46
|
+
function getBuiltinModels(): Promise<readonly BuiltinCatalogModel[]> {
|
|
47
|
+
builtinModelsPromise ??= ModelRuntime.create({
|
|
48
|
+
credentials: emptyCredentialStore,
|
|
49
|
+
modelsPath: null,
|
|
50
|
+
allowModelNetwork: false,
|
|
51
|
+
}).then((runtime) =>
|
|
52
|
+
runtime.getModels().map((model) => ({
|
|
53
|
+
provider: String(model.provider),
|
|
54
|
+
api: String(model.api),
|
|
55
|
+
baseUrl: model.baseUrl,
|
|
56
|
+
})),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return builtinModelsPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getBuiltinProviderIds(): Promise<ReadonlySet<string>> {
|
|
63
|
+
builtinProviderIdsPromise ??= getBuiltinModels().then(
|
|
64
|
+
(models) => new Set(models.map((model) => model.provider)),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return builtinProviderIdsPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isBuiltinProviderId(providerId: string): Promise<boolean> {
|
|
71
|
+
return (await getBuiltinProviderIds()).has(providerId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function asSupportedApiKind(value: string | undefined): ApiKind | undefined {
|
|
75
|
+
return value && SUPPORTED_API_KINDS.has(value as ApiKind)
|
|
76
|
+
? (value as ApiKind)
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getBuiltinProviderDefaults(
|
|
81
|
+
providerId: string,
|
|
82
|
+
): Promise<BuiltinProviderDefaults | undefined> {
|
|
83
|
+
const firstModel = (await getBuiltinModels()).find(
|
|
84
|
+
(model) => model.provider === providerId,
|
|
85
|
+
);
|
|
86
|
+
if (!firstModel) return undefined;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
api: asSupportedApiKind(firstModel.api),
|
|
90
|
+
baseUrl: firstModel.baseUrl,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ClaudeCode 客户端兼容增强:让选择 ClaudeCode 请求头 profile 的 Anthropic 接入
|
|
2
|
+
// 同时具备 Snow CLI/ClaudeCode 中转常见的 body metadata 信号。
|
|
3
|
+
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { isObjectRecord } from "./common.ts";
|
|
6
|
+
import { resolveClientHeaderProfile } from "./presets/client-headers.ts";
|
|
7
|
+
import type { StateDocument } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
type ActiveModelRef = {
|
|
10
|
+
provider: string;
|
|
11
|
+
id: string;
|
|
12
|
+
api: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type PayloadRecord = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
let currentClaudeCodeUserId = createClaudeCodeUserId();
|
|
18
|
+
|
|
19
|
+
function createClaudeCodeUserId(): string {
|
|
20
|
+
const sessionId = randomUUID();
|
|
21
|
+
const hash = createHash("sha256").update(`anthropic_user_${sessionId}`).digest("hex");
|
|
22
|
+
return `user_${hash}_account__session_${sessionId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resetClaudeCodeMetadataSession(): void {
|
|
26
|
+
currentClaudeCodeUserId = createClaudeCodeUserId();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPayloadRecord(payload: unknown): payload is PayloadRecord {
|
|
30
|
+
return isObjectRecord(payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function injectClaudeCodeMetadata(
|
|
34
|
+
payload: unknown,
|
|
35
|
+
model: ActiveModelRef | undefined,
|
|
36
|
+
state: StateDocument,
|
|
37
|
+
): PayloadRecord | undefined {
|
|
38
|
+
if (!model || model.api !== "anthropic-messages") return undefined;
|
|
39
|
+
if (!isPayloadRecord(payload)) return undefined;
|
|
40
|
+
if (payload.model !== model.id) return undefined;
|
|
41
|
+
|
|
42
|
+
const provider = state.providers[model.provider];
|
|
43
|
+
if (!provider) return undefined;
|
|
44
|
+
if (resolveClientHeaderProfile(provider.clientHeaderProfile, "anthropic-messages") !== "claude-code") return undefined;
|
|
45
|
+
|
|
46
|
+
const metadata = isObjectRecord(payload.metadata) ? payload.metadata : {};
|
|
47
|
+
return {
|
|
48
|
+
...payload,
|
|
49
|
+
// ARN/new-api 的 ClaudeCode-only 限制会检查 body 信号;只补 metadata,
|
|
50
|
+
// 不改写 system prompt,避免降低 Pi 系统提示词优先级。
|
|
51
|
+
metadata: {
|
|
52
|
+
...metadata,
|
|
53
|
+
user_id: currentClaudeCodeUserId,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|