set-agent-provider 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 +21 -0
- package/README.en.md +164 -0
- package/README.md +157 -0
- package/bin/set-agent-provider.js +7 -0
- package/package.json +30 -0
- package/src/adapters/claude.js +64 -0
- package/src/adapters/codex.js +171 -0
- package/src/adapters/dsh.js +158 -0
- package/src/adapters/index.js +15 -0
- package/src/adapters/opencode.js +78 -0
- package/src/adapters/pi.js +73 -0
- package/src/apply.js +68 -0
- package/src/backup.js +28 -0
- package/src/cli/args.js +43 -0
- package/src/cli/help.js +33 -0
- package/src/cli/prompt.js +69 -0
- package/src/config/baseurl.js +49 -0
- package/src/config/load.js +50 -0
- package/src/config/name.js +18 -0
- package/src/config/resolve.js +58 -0
- package/src/config/schema.js +47 -0
- package/src/discover.js +34 -0
- package/src/domain/resolve.js +99 -0
- package/src/index.js +48 -0
- package/src/models/fetch.js +43 -0
- package/src/models/select.js +120 -0
- package/src/output.js +47 -0
- package/src/status.js +51 -0
- package/src/util/fs.js +40 -0
- package/src/util/http.js +62 -0
- package/src/util/paths.js +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<a href="README.en.md">English</a> | <a href="README.md">简体中文</a>
|
|
2
|
+
|
|
3
|
+
# set-agent-provider
|
|
4
|
+
|
|
5
|
+
Set up **every** installed AI CLI for a model provider — in one command.
|
|
6
|
+
|
|
7
|
+
Give it a provider (even just a domain) and it auto-detects the base URL and models,
|
|
8
|
+
then writes the native config for every AI CLI it finds — Claude Code, Codex,
|
|
9
|
+
OpenCode, pi, and dsh — configuring all your clients in one command:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx set-agent-provider -D api.deepseek.com
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Switching to any OpenAI-compatible provider is just a different domain:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx set-agent-provider -D api.example.com -K sk-xxxxxxxx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Install / Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx set-agent-provider --name foo \
|
|
25
|
+
--baseUrl https://api.example.com/v1 \
|
|
26
|
+
--apiKey sk-xxxxxxxx \
|
|
27
|
+
--models '[{"id":"gpt-5.6-sol","name":"GPT-5.6 SOL","reasoning":true,"limit":{"context":272000,"output":128000}}]'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Short flags work too:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx set-agent-provider -N foo -U https://api.example.com/v1 -K sk-xxxxxxxx \
|
|
34
|
+
-M '[{"id":"gpt-5.6-sol","name":"GPT-5.6 SOL","reasoning":true,"limit":{"context":272000,"output":128000}}]'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run with no arguments to get an interactive English wizard:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx set-agent-provider
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
| Flag | Short | Description |
|
|
46
|
+
|------|-------|-------------|
|
|
47
|
+
| `--name` | `-N` | Provider name (default: hostname of `baseUrl`) |
|
|
48
|
+
| `--baseUrl` | `-U` | Provider base URL (required; prompted if omitted interactively; full URL) |
|
|
49
|
+
| `--apiKey` | `-K` | Provider API key (optional; prompted if omitted, may be blank) |
|
|
50
|
+
| `--models` | `-M` | Models as strict JSON |
|
|
51
|
+
| `--target` | `-T` | Comma-separated targets: `claude,codex,opencode,pi,dsh` |
|
|
52
|
+
| `--config` | `-C` | Config source: local file, URL, or bare domain (mutually exclusive with `-D`) |
|
|
53
|
+
| `--domain` | `-D` | Auto-detect from a bare domain (config, then endpoint probe) |
|
|
54
|
+
| `--status` | | Show the current provider config for each installed CLI |
|
|
55
|
+
| `--help` | `-h` | Show help |
|
|
56
|
+
| `--version` | `-v` | Show version |
|
|
57
|
+
|
|
58
|
+
### Models
|
|
59
|
+
|
|
60
|
+
`--models` takes a strict JSON array. Each entry:
|
|
61
|
+
|
|
62
|
+
| Field | Description |
|
|
63
|
+
|-------|-------------|
|
|
64
|
+
| `id` | Model id |
|
|
65
|
+
| `name` | Display name |
|
|
66
|
+
| `reasoning` | Whether the model is a reasoning model |
|
|
67
|
+
| `limit.context` | Context window |
|
|
68
|
+
| `limit.output` | Max output tokens |
|
|
69
|
+
|
|
70
|
+
If `--models` is omitted, the tool requests `{baseUrl}/v1/models` (with the API
|
|
71
|
+
key) and lets you pick models interactively (Space to toggle, Enter to confirm).
|
|
72
|
+
Any failure (including 401) or an empty list is an error — pass `-M/--models` to
|
|
73
|
+
provide the list directly.
|
|
74
|
+
|
|
75
|
+
### Config source
|
|
76
|
+
|
|
77
|
+
`--config` accepts:
|
|
78
|
+
|
|
79
|
+
- a local file path: `-C ./config.json`
|
|
80
|
+
- a full URL: `-C https://example.com/set-agent-provider-config.json`
|
|
81
|
+
- a bare domain: `-C example.com` → tries `https://` then `http://` at `/set-agent-provider-config.json`
|
|
82
|
+
|
|
83
|
+
The config file has the same fields as the flags:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"name": "example",
|
|
88
|
+
"baseUrl": "https://api.example.com/v1",
|
|
89
|
+
"apiKey": "sk-xxxxxxxx",
|
|
90
|
+
"models": [
|
|
91
|
+
{ "id": "gpt-5.6-sol", "name": "GPT-5.6 SOL", "reasoning": true, "limit": { "context": 272000, "output": 128000 } }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Command-line flags override config-file values (e.g. `-C example.com -K sk-other`
|
|
97
|
+
keeps the remote config but uses your key). `-C` and `-D` are mutually exclusive;
|
|
98
|
+
`-U` can be combined with either and takes precedence.
|
|
99
|
+
|
|
100
|
+
### Auto-detect from a domain
|
|
101
|
+
|
|
102
|
+
`--domain` takes just a bare domain and resolves the provider for you:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx set-agent-provider -D api.deepseek.com
|
|
106
|
+
npx set-agent-provider -D api.deepseek.com -K sk-xxxxxxxx
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It probes in this order:
|
|
110
|
+
|
|
111
|
+
1. `https://{domain}/set-agent-provider-config.json`, then `http://...` — if it
|
|
112
|
+
returns a valid JSON object, that config is used directly (same shape as
|
|
113
|
+
`--config`).
|
|
114
|
+
2. Otherwise it detects an OpenAI-compatible API by POSTing to
|
|
115
|
+
`/v1/chat/completions` over `https` then `http`; if the status is not
|
|
116
|
+
`404/405/501` and the body parses as JSON (**401 also counts as existing**),
|
|
117
|
+
the provider name is derived from `{domain}` and the base URL is
|
|
118
|
+
`{scheme}://{domain}/v1`.
|
|
119
|
+
3. Models are then fetched from `{baseUrl}/v1/models` and you pick them interactively.
|
|
120
|
+
|
|
121
|
+
A config reached via `-D` that is not valid JSON (e.g. a gateway HTML fallback)
|
|
122
|
+
is treated as absent and degrades to endpoint detection; a config explicitly
|
|
123
|
+
given via `-C` is still a hard error. `--domain` and `--config` are mutually
|
|
124
|
+
exclusive; `-U` can be combined and takes precedence.
|
|
125
|
+
|
|
126
|
+
## Supported CLIs
|
|
127
|
+
|
|
128
|
+
| Target | Config written |
|
|
129
|
+
|--------|----------------|
|
|
130
|
+
| `claude` | `~/.claude/settings.json` (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`) |
|
|
131
|
+
| `codex` | `~/.codex/config.toml` (`[model_providers.*]`, `model`, `model_provider`) + `~/.codex/auth.json` |
|
|
132
|
+
| `opencode` | `~/.config/opencode/opencode.json` (`provider.*`, `model`) |
|
|
133
|
+
| `pi` | `~/.pi/agent/models.json` (`providers.*`) |
|
|
134
|
+
| `dsh` | `~/.dsh/settings.yaml` (`llm-pi-ai`, `agent-default-model`) + `~/.dsh/.credentials.yaml` |
|
|
135
|
+
|
|
136
|
+
By default every installed CLI is configured. Use `--target` to limit it.
|
|
137
|
+
|
|
138
|
+
## Safety
|
|
139
|
+
|
|
140
|
+
- Every config file is backed up before it is overwritten, saved next to the
|
|
141
|
+
original as `<filename>.bak-<YYYY-MM-DD_HH-mm-ss>` (local time).
|
|
142
|
+
- API keys are never printed — output always masks them.
|
|
143
|
+
- A failure on one target does not stop the others; the final summary lists what
|
|
144
|
+
succeeded and what failed.
|
|
145
|
+
|
|
146
|
+
## Codex Responses detection
|
|
147
|
+
|
|
148
|
+
Codex needs to know whether an endpoint speaks the Responses API. Interactively
|
|
149
|
+
you are offered three choices (probe the endpoint only, test the default model
|
|
150
|
+
with a real request, or skip). Non-interactively the tool probes the endpoint
|
|
151
|
+
only. The result sets `wire_api` to `responses` or `chat`.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
Zero dependencies; plain CommonJS; Node >= 14.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
node bin/set-agent-provider.js --help
|
|
159
|
+
node bin/set-agent-provider.js --status
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<a href="README.en.md">English</a> | <a href="README.md">简体中文</a>
|
|
2
|
+
|
|
3
|
+
# set-agent-provider
|
|
4
|
+
|
|
5
|
+
为某个模型提供商,一键设置机器上**所有**已安装的 AI CLI。
|
|
6
|
+
|
|
7
|
+
给一个提供商(哪怕只是一个域名),它就会自动探测出 base URL 与模型,
|
|
8
|
+
并为每个检测到的 AI CLI 写入各自的原生配置 —— Claude Code、Codex、OpenCode、
|
|
9
|
+
pi 和 dsh —— 一键完成所有客户端的配置:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx set-agent-provider -D api.deepseek.com
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
换任何 OpenAI 兼容的提供商,也只需换一个域名:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx set-agent-provider -D api.example.com -K sk-xxxxxxxx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 安装 / 使用
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx set-agent-provider --name foo \
|
|
25
|
+
--baseUrl https://api.example.com/v1 \
|
|
26
|
+
--apiKey sk-xxxxxxxx \
|
|
27
|
+
--models '[{"id":"gpt-5.6-sol","name":"GPT-5.6 SOL","reasoning":true,"limit":{"context":272000,"output":128000}}]'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
也支持短选项:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx set-agent-provider -N foo -U https://api.example.com/v1 -K sk-xxxxxxxx \
|
|
34
|
+
-M '[{"id":"gpt-5.6-sol","name":"GPT-5.6 SOL","reasoning":true,"limit":{"context":272000,"output":128000}}]'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
不带任何参数运行,会进入交互式英文向导:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx set-agent-provider
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 选项
|
|
44
|
+
|
|
45
|
+
| 选项 | 短选项 | 说明 |
|
|
46
|
+
|------|--------|------|
|
|
47
|
+
| `--name` | `-N` | 提供商名称(默认取 `baseUrl` 的主机名) |
|
|
48
|
+
| `--baseUrl` | `-U` | 提供商 base URL(必填;交互模式下缺省会提示输入;需完整 URL) |
|
|
49
|
+
| `--apiKey` | `-K` | 提供商 API key(可选;缺省时交互式提示,可留空) |
|
|
50
|
+
| `--models` | `-M` | 模型列表,严格 JSON |
|
|
51
|
+
| `--target` | `-T` | 逗号分隔的目标:`claude,codex,opencode,pi,dsh` |
|
|
52
|
+
| `--config` | `-C` | 配置来源:本地文件、URL 或裸域名(与 `-D` 互斥) |
|
|
53
|
+
| `--domain` | `-D` | 从裸域名自动探测提供商(先配置,再端点探测) |
|
|
54
|
+
| `--status` | | 显示每个已安装 CLI 当前的提供商配置 |
|
|
55
|
+
| `--help` | `-h` | 显示帮助 |
|
|
56
|
+
| `--version` | `-v` | 显示版本 |
|
|
57
|
+
|
|
58
|
+
### 模型
|
|
59
|
+
|
|
60
|
+
`--models` 接收严格的 JSON 数组。每一项:
|
|
61
|
+
|
|
62
|
+
| 字段 | 说明 |
|
|
63
|
+
|------|------|
|
|
64
|
+
| `id` | 模型 id |
|
|
65
|
+
| `name` | 显示名称 |
|
|
66
|
+
| `reasoning` | 是否为推理模型 |
|
|
67
|
+
| `limit.context` | 上下文窗口 |
|
|
68
|
+
| `limit.output` | 最大输出 token 数 |
|
|
69
|
+
|
|
70
|
+
如果省略 `--models`,工具会携带 API key 请求 `{baseUrl}/v1/models`,
|
|
71
|
+
并让你交互式选择(空格切换、回车确认)。任何失败(含 401)或空列表都会报错,
|
|
72
|
+
此时请用 `-M/--models` 直接提供模型列表。
|
|
73
|
+
|
|
74
|
+
### 配置来源
|
|
75
|
+
|
|
76
|
+
`--config` 支持:
|
|
77
|
+
|
|
78
|
+
- 本地文件路径:`-C ./config.json`
|
|
79
|
+
- 完整 URL:`-C https://example.com/set-agent-provider-config.json`
|
|
80
|
+
- 裸域名:`-C example.com` → 依次尝试 `https://` 和 `http://` 下的 `/set-agent-provider-config.json`
|
|
81
|
+
|
|
82
|
+
配置文件字段与命令行选项一致:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"name": "example",
|
|
87
|
+
"baseUrl": "https://api.example.com/v1",
|
|
88
|
+
"apiKey": "sk-xxxxxxxx",
|
|
89
|
+
"models": [
|
|
90
|
+
{ "id": "gpt-5.6-sol", "name": "GPT-5.6 SOL", "reasoning": true, "limit": { "context": 272000, "output": 128000 } }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
命令行选项会覆盖配置文件中的值(例如 `-C example.com -K sk-other`
|
|
96
|
+
会保留远程配置但使用你自己的 key)。`-C` 与 `-D` 互斥,`-U` 可与二者叠加并优先生效。
|
|
97
|
+
|
|
98
|
+
### 从域名自动探测
|
|
99
|
+
|
|
100
|
+
`--domain` 只接收一个裸域名,自动解析出提供商:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx set-agent-provider -D api.deepseek.com
|
|
104
|
+
npx set-agent-provider -D api.deepseek.com -K sk-xxxxxxxx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
探测顺序如下:
|
|
108
|
+
|
|
109
|
+
1. 先请求 `https://{domain}/set-agent-provider-config.json`,失败再试 `http://...`
|
|
110
|
+
—— 若返回合法 JSON 对象,则直接使用该配置(格式与 `--config` 相同)。
|
|
111
|
+
2. 否则检测是否为 OpenAI 兼容 API:依次通过 `https`、`http` 对
|
|
112
|
+
`POST /v1/chat/completions` 发起请求;只要状态不是 `404/405/501` 且响应体可解析为
|
|
113
|
+
JSON(**401 也视为端点存在**),即以 `{domain}` 派生名称,base URL 为
|
|
114
|
+
`{scheme}://{domain}/v1`。
|
|
115
|
+
3. 随后从 `{baseUrl}/v1/models` 获取模型,由你交互式选择。
|
|
116
|
+
|
|
117
|
+
`-D` 探测到的配置若为非法 JSON(例如网关返回 HTML 兜底页)会被视为不存在并降级为端点
|
|
118
|
+
探测;而 `-C` 显式指定的配置若非法 JSON 仍属硬错误。`--domain` 与 `--config` 互斥,
|
|
119
|
+
`-U` 可与二者叠加并优先。
|
|
120
|
+
|
|
121
|
+
## 支持的 CLI
|
|
122
|
+
|
|
123
|
+
| 目标 | 写入的配置 |
|
|
124
|
+
|------|-----------|
|
|
125
|
+
| `claude` | `~/.claude/settings.json`(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_MODEL`、`ANTHROPIC_DEFAULT_HAIKU_MODEL`、`ANTHROPIC_DEFAULT_SONNET_MODEL`、`ANTHROPIC_DEFAULT_OPUS_MODEL`) |
|
|
126
|
+
| `codex` | `~/.codex/config.toml`(`[model_providers.*]`、`model`、`model_provider`)+ `~/.codex/auth.json` |
|
|
127
|
+
| `opencode` | `~/.config/opencode/opencode.json`(`provider.*`、`model`) |
|
|
128
|
+
| `pi` | `~/.pi/agent/models.json`(`providers.*`) |
|
|
129
|
+
| `dsh` | `~/.dsh/settings.yaml`(`llm-pi-ai`、`agent-default-model`)+ `~/.dsh/.credentials.yaml` |
|
|
130
|
+
|
|
131
|
+
默认会配置所有已安装的 CLI。可用 `--target` 限定范围。
|
|
132
|
+
|
|
133
|
+
## 安全
|
|
134
|
+
|
|
135
|
+
- 每个配置文件在覆盖前都会备份,保存在原文件旁,命名为
|
|
136
|
+
`<文件名>.bak-<YYYY-MM-DD_HH-mm-ss>`(本地时间)。
|
|
137
|
+
- API key 永不打印 —— 输出一律做脱敏处理。
|
|
138
|
+
- 某个目标失败不会中断其他目标;最后的汇总会列出成功与失败项。
|
|
139
|
+
|
|
140
|
+
## Codex Responses 探测
|
|
141
|
+
|
|
142
|
+
Codex 需要知道某个端点是否使用 Responses API。交互模式下会提供三种选择
|
|
143
|
+
(仅探测端点、用默认模型发一次真实请求测试、或跳过)。非交互模式下仅探测端点。
|
|
144
|
+
结果决定 `wire_api` 为 `responses` 还是 `chat`。
|
|
145
|
+
|
|
146
|
+
## 开发
|
|
147
|
+
|
|
148
|
+
零依赖;纯 CommonJS;Node >= 14。
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
node bin/set-agent-provider.js --help
|
|
152
|
+
node bin/set-agent-provider.js --status
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 许可证
|
|
156
|
+
|
|
157
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "set-agent-provider",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Configure a model provider across all installed AI CLIs with one command",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"set-agent-provider": "bin/set-agent-provider.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"README.en.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=14"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"ai",
|
|
22
|
+
"llm",
|
|
23
|
+
"provider",
|
|
24
|
+
"claude",
|
|
25
|
+
"codex",
|
|
26
|
+
"opencode",
|
|
27
|
+
"pi",
|
|
28
|
+
"dsh"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { home } = require('../util/paths');
|
|
5
|
+
const { exists, readJson, writeJson } = require('../util/fs');
|
|
6
|
+
|
|
7
|
+
const ID = 'claude';
|
|
8
|
+
|
|
9
|
+
function file() {
|
|
10
|
+
return path.join(home(), '.claude', 'settings.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stripV1(url) {
|
|
14
|
+
let u = String(url).trim().replace(/\/+$/, '');
|
|
15
|
+
u = u.replace(/\/v1$/i, '');
|
|
16
|
+
return u + '/';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function write(ctx) {
|
|
20
|
+
const f = file();
|
|
21
|
+
let cfg = {};
|
|
22
|
+
if (exists(f)) {
|
|
23
|
+
try { cfg = readJson(f); } catch (e) { cfg = {}; }
|
|
24
|
+
}
|
|
25
|
+
if (!cfg.env || typeof cfg.env !== 'object') cfg.env = {};
|
|
26
|
+
|
|
27
|
+
cfg.env.ANTHROPIC_BASE_URL = stripV1(ctx.baseUrl);
|
|
28
|
+
if (ctx.apiKey) cfg.env.ANTHROPIC_AUTH_TOKEN = ctx.apiKey;
|
|
29
|
+
if (ctx.defaultModel) {
|
|
30
|
+
cfg.env.ANTHROPIC_MODEL = ctx.defaultModel;
|
|
31
|
+
cfg.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = ctx.defaultModel;
|
|
32
|
+
cfg.env.ANTHROPIC_DEFAULT_SONNET_MODEL = ctx.defaultModel;
|
|
33
|
+
cfg.env.ANTHROPIC_DEFAULT_OPUS_MODEL = ctx.defaultModel;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return writeJson(f, cfg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readStatus() {
|
|
40
|
+
const f = file();
|
|
41
|
+
if (!exists(f)) return null;
|
|
42
|
+
let cfg;
|
|
43
|
+
try { cfg = readJson(f); } catch (e) { return null; }
|
|
44
|
+
const env = cfg.env || {};
|
|
45
|
+
if (!env.ANTHROPIC_BASE_URL && !env.ANTHROPIC_AUTH_TOKEN) return null;
|
|
46
|
+
return {
|
|
47
|
+
configured: true,
|
|
48
|
+
name: 'claude',
|
|
49
|
+
baseUrl: env.ANTHROPIC_BASE_URL,
|
|
50
|
+
defaultModel: env.ANTHROPIC_MODEL || null,
|
|
51
|
+
modelCount: env.ANTHROPIC_MODEL ? 1 : 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
id: ID,
|
|
57
|
+
name: 'Claude Code',
|
|
58
|
+
configPaths: ['~/.claude/settings.json', '~/.claude.json', '~/.claude'],
|
|
59
|
+
supports: { reasoning: false, limit: false },
|
|
60
|
+
writeFiles: function () { return [file()]; },
|
|
61
|
+
write: write,
|
|
62
|
+
readStatus: readStatus,
|
|
63
|
+
stripV1: stripV1
|
|
64
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { home } = require('../util/paths');
|
|
5
|
+
const { exists, readText, writeText, readJson, writeJson } = require('../util/fs');
|
|
6
|
+
const { post } = require('../util/http');
|
|
7
|
+
const { promptChoice } = require('../models/select');
|
|
8
|
+
|
|
9
|
+
const ID = 'codex';
|
|
10
|
+
const ENV_KEY = 'OPENAI_API_KEY';
|
|
11
|
+
|
|
12
|
+
function configFile() { return path.join(home(), '.codex', 'config.toml'); }
|
|
13
|
+
function authFile() { return path.join(home(), '.codex', 'auth.json'); }
|
|
14
|
+
|
|
15
|
+
function tomlStr(s) {
|
|
16
|
+
return JSON.stringify(String(s));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sanitizeKey(name) {
|
|
20
|
+
const k = String(name).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
21
|
+
return k || 'provider';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function headerPath(line) {
|
|
25
|
+
const m = String(line).match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/);
|
|
26
|
+
return m ? m[1] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setTopKey(lines, key, value) {
|
|
30
|
+
if (value == null || value === '') return lines;
|
|
31
|
+
const re = new RegExp('^\\s*' + key + '\\s*=');
|
|
32
|
+
const out = lines.filter(function (l) { return !re.test(l); });
|
|
33
|
+
out.push(key + ' = ' + tomlStr(value));
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeProviderTables(lines, key) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let skip = false;
|
|
40
|
+
for (const l of lines) {
|
|
41
|
+
const h = headerPath(l);
|
|
42
|
+
if (h != null) {
|
|
43
|
+
const norm = h.replace(/"/g, '');
|
|
44
|
+
skip = norm === 'model_providers.' + key || norm.indexOf('model_providers.' + key + '.') === 0;
|
|
45
|
+
if (skip) continue;
|
|
46
|
+
}
|
|
47
|
+
if (!skip) out.push(l);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function probeEndpoint(baseUrl, apiKey) {
|
|
53
|
+
const url = String(baseUrl).replace(/\/+$/, '') + '/responses';
|
|
54
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
55
|
+
if (apiKey) headers.Authorization = 'Bearer ' + apiKey;
|
|
56
|
+
try {
|
|
57
|
+
const res = await post(url, { headers: headers, body: { model: '__probe__', input: 'ping' }, timeout: 10000 });
|
|
58
|
+
return { exists: res.status !== 404 && res.status !== 405 && res.status !== 501, status: res.status };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return { exists: false, error: e.message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function probeConversation(baseUrl, apiKey, model) {
|
|
65
|
+
const url = String(baseUrl).replace(/\/+$/, '') + '/responses';
|
|
66
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
67
|
+
if (apiKey) headers.Authorization = 'Bearer ' + apiKey;
|
|
68
|
+
try {
|
|
69
|
+
const res = await post(url, { headers: headers, body: { model: model, input: 'ping' }, timeout: 30000 });
|
|
70
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { ok: false, error: e.message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function resolveOptions(ctx) {
|
|
77
|
+
let mode = 'endpoint';
|
|
78
|
+
if (ctx.interactive) {
|
|
79
|
+
const idx = await promptChoice('Codex: check Responses API support?', [
|
|
80
|
+
'Detect endpoint only (POST /v1/responses)',
|
|
81
|
+
'Test the default model with a real request',
|
|
82
|
+
'Skip detection'
|
|
83
|
+
]);
|
|
84
|
+
if (idx === 1) mode = 'conversation';
|
|
85
|
+
else if (idx === 2) mode = 'skip';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (mode === 'skip') return { wireApi: 'responses' };
|
|
89
|
+
if (mode === 'conversation') {
|
|
90
|
+
const r = await probeConversation(ctx.baseUrl, ctx.apiKey, ctx.defaultModel);
|
|
91
|
+
return { wireApi: r.ok ? 'responses' : 'chat', detection: r, detectionMode: mode };
|
|
92
|
+
}
|
|
93
|
+
const r = await probeEndpoint(ctx.baseUrl, ctx.apiKey);
|
|
94
|
+
return { wireApi: r.exists ? 'responses' : 'chat', detection: r, detectionMode: mode };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function write(ctx) {
|
|
98
|
+
const f = configFile();
|
|
99
|
+
let text = exists(f) ? readText(f) : '';
|
|
100
|
+
const key = sanitizeKey(ctx.name);
|
|
101
|
+
const wireApi = ctx.wireApi || 'responses';
|
|
102
|
+
|
|
103
|
+
const lines = text.length ? text.split(/\r?\n/) : [];
|
|
104
|
+
let firstSection = -1;
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
if (/^\s*\[/.test(lines[i])) { firstSection = i; break; }
|
|
107
|
+
}
|
|
108
|
+
if (firstSection === -1) firstSection = lines.length;
|
|
109
|
+
|
|
110
|
+
let top = lines.slice(0, firstSection);
|
|
111
|
+
let rest = lines.slice(firstSection);
|
|
112
|
+
|
|
113
|
+
top = setTopKey(top, 'model', ctx.defaultModel);
|
|
114
|
+
top = setTopKey(top, 'model_provider', key);
|
|
115
|
+
rest = removeProviderTables(rest, key);
|
|
116
|
+
|
|
117
|
+
const section = [
|
|
118
|
+
'[model_providers.' + key + ']',
|
|
119
|
+
'name = ' + tomlStr(ctx.name),
|
|
120
|
+
'base_url = ' + tomlStr(String(ctx.baseUrl).replace(/\/+$/, '')),
|
|
121
|
+
'env_key = ' + tomlStr(ENV_KEY),
|
|
122
|
+
'wire_api = ' + tomlStr(wireApi),
|
|
123
|
+
'requires_openai_auth = true'
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const topText = top.join('\n').trim();
|
|
127
|
+
const restText = rest.join('\n').trim();
|
|
128
|
+
const parts = [];
|
|
129
|
+
if (topText) parts.push(topText);
|
|
130
|
+
parts.push(section.join('\n'));
|
|
131
|
+
if (restText) parts.push(restText);
|
|
132
|
+
let changed = writeText(f, parts.join('\n\n') + '\n');
|
|
133
|
+
|
|
134
|
+
if (ctx.apiKey) {
|
|
135
|
+
const af = authFile();
|
|
136
|
+
let auth = {};
|
|
137
|
+
if (exists(af)) {
|
|
138
|
+
try { auth = readJson(af); } catch (e) { auth = {}; }
|
|
139
|
+
}
|
|
140
|
+
auth.auth_mode = auth.auth_mode || 'apikey';
|
|
141
|
+
auth[ENV_KEY] = ctx.apiKey;
|
|
142
|
+
if (writeJson(af, auth)) changed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return changed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readStatus() {
|
|
149
|
+
const f = configFile();
|
|
150
|
+
if (!exists(f)) return null;
|
|
151
|
+
const text = readText(f);
|
|
152
|
+
const mp = (text.match(/^\s*model_provider\s*=\s*"([^"]+)"/m) || [])[1];
|
|
153
|
+
const model = (text.match(/^\s*model\s*=\s*"([^"]+)"/m) || [])[1];
|
|
154
|
+
if (!mp) return null;
|
|
155
|
+
const re = new RegExp('\\[model_providers\\."?' + mp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '"?\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"');
|
|
156
|
+
const baseUrl = (text.match(re) || [])[1];
|
|
157
|
+
return { configured: true, name: mp, baseUrl: baseUrl, defaultModel: model || null };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
id: ID,
|
|
162
|
+
name: 'Codex',
|
|
163
|
+
configPaths: ['~/.codex/config.toml', '~/.codex'],
|
|
164
|
+
supports: { reasoning: false, limit: false },
|
|
165
|
+
writeFiles: function () { return [configFile(), authFile()]; },
|
|
166
|
+
resolveOptions: resolveOptions,
|
|
167
|
+
probeEndpoint: probeEndpoint,
|
|
168
|
+
probeConversation: probeConversation,
|
|
169
|
+
write: write,
|
|
170
|
+
readStatus: readStatus
|
|
171
|
+
};
|