@gnahz77/opencode-copilot-multi-auth 0.1.3 → 0.1.5
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/README.md +38 -14
- package/README.zh.md +218 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +18 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -3
- package/dist/pool.d.ts +4 -0
- package/dist/pool.js +332 -30
- package/dist/tui.js +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/usage.js +1 -1
- package/index.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# opencode-copilot-multi-auth
|
|
2
2
|
|
|
3
|
+
[中文文档](README.zh.md)
|
|
4
|
+
|
|
3
5
|
Package on npm: https://www.npmjs.com/package/@gnahz77/opencode-copilot-multi-auth
|
|
4
6
|
|
|
5
7
|
This fork replaces the older GitHub Copilot chat-auth flow with the newer Copilot CLI-style OAuth flow and makes `opencode` use the live Copilot model metadata for your account.
|
|
@@ -12,7 +14,7 @@ Add the plugin to your `opencode` config:
|
|
|
12
14
|
{
|
|
13
15
|
"$schema": "https://opencode.ai/config.json",
|
|
14
16
|
"plugin": [
|
|
15
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
17
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.5"
|
|
16
18
|
]
|
|
17
19
|
}
|
|
18
20
|
```
|
|
@@ -25,13 +27,16 @@ If you also want the TUI command support provided by this package, add the same
|
|
|
25
27
|
{
|
|
26
28
|
"$schema": "https://opencode.ai/tui.json",
|
|
27
29
|
"plugin": [
|
|
28
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
30
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.5"
|
|
29
31
|
]
|
|
30
32
|
}
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
After restarting OpenCode TUI, you can open the command palette and run `copilot-usage`, or type `/copilot-usage`.
|
|
34
|
-
The plugin will open a popup dialog showing all accounts currently
|
|
36
|
+
The plugin will open a popup dialog showing all accounts currently loaded from split local storage:
|
|
37
|
+
|
|
38
|
+
- OAuth/account data: `~/.local/share/opencode/copilot-auth.json`
|
|
39
|
+
- Per-account routing policy: `~/.config/opencode/copilot-auth.json`
|
|
35
40
|
|
|
36
41
|
For local development before publishing, you can load the file directly:
|
|
37
42
|
|
|
@@ -66,7 +71,9 @@ This package now includes a TUI command named `copilot-usage`.
|
|
|
66
71
|
|
|
67
72
|
What it does:
|
|
68
73
|
|
|
69
|
-
- Reads every Copilot account
|
|
74
|
+
- Reads every Copilot account from the merged local pool assembled from:
|
|
75
|
+
- `~/.local/share/opencode/copilot-auth.json` (OAuth/account data)
|
|
76
|
+
- `~/.config/opencode/copilot-auth.json` (routing policy)
|
|
70
77
|
- Calls the GitHub Copilot entitlement endpoint `/copilot_internal/user` for each account using that account's OAuth token
|
|
71
78
|
- Opens a popup dialog showing, for each account:
|
|
72
79
|
- account name
|
|
@@ -147,10 +154,16 @@ The plugin intentionally does not try to change the `opencode` core UI. So the v
|
|
|
147
154
|
|
|
148
155
|
## Multi-Account Support
|
|
149
156
|
|
|
150
|
-
This fork
|
|
157
|
+
This fork keeps Copilot account state in split local files:
|
|
158
|
+
|
|
159
|
+
- OAuth-required account data in `~/.local/share/opencode/copilot-auth.json`
|
|
160
|
+
- Per-account routing policy in `~/.config/opencode/copilot-auth.json`
|
|
161
|
+
|
|
162
|
+
On startup, the plugin automatically migrates legacy single-file storage into this two-file layout.
|
|
163
|
+
|
|
151
164
|
Each successful OAuth login updates that pool automatically: logging in with a new deployment-scoped account appends a new record, and logging in again with the same GitHub identity on the same deployment updates the existing record instead of creating a duplicate.
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
At runtime, the plugin merges both files into the same in-memory `version: 2` pool shape (`accounts`) used for routing.
|
|
154
167
|
|
|
155
168
|
| Field | Meaning |
|
|
156
169
|
| --- | --- |
|
|
@@ -165,20 +178,14 @@ Automatic routing works on the raw Copilot model IDs that `opencode` already use
|
|
|
165
178
|
|
|
166
179
|
Wildcard matching is case-sensitive and currently supports `*` for zero or more characters anywhere in the pattern. For example, `claude-*` matches `claude-sonnet-4.6` and `claude-opus-4.6`.
|
|
167
180
|
|
|
168
|
-
Example
|
|
181
|
+
Example auth file (`~/.local/share/opencode/copilot-auth.json`):
|
|
169
182
|
|
|
170
183
|
```json
|
|
171
184
|
{
|
|
172
|
-
"version":
|
|
185
|
+
"version": 2,
|
|
173
186
|
"accounts": [
|
|
174
187
|
{
|
|
175
188
|
"key": "github.com:12345678",
|
|
176
|
-
"id": "work",
|
|
177
|
-
"name": "Work",
|
|
178
|
-
"enabled": true,
|
|
179
|
-
"priority": 100,
|
|
180
|
-
"allowlist": ["claude-*"],
|
|
181
|
-
"blocklist": [],
|
|
182
189
|
"deployment": "github.com",
|
|
183
190
|
"domain": "github.com",
|
|
184
191
|
"baseUrl": "https://api.githubcopilot.com",
|
|
@@ -196,3 +203,20 @@ Example pool file:
|
|
|
196
203
|
]
|
|
197
204
|
}
|
|
198
205
|
```
|
|
206
|
+
|
|
207
|
+
Example policy file (`~/.config/opencode/copilot-auth.json`):
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"version": 2,
|
|
212
|
+
"accounts": [
|
|
213
|
+
{
|
|
214
|
+
"key": "github.com:12345678",
|
|
215
|
+
"enabled": true,
|
|
216
|
+
"priority": 100,
|
|
217
|
+
"allowlist": ["claude-*"],
|
|
218
|
+
"blocklist": []
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
```
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# opencode-copilot-multi-auth
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
npm 上的包主页: https://www.npmjs.com/package/@gnahz77/opencode-copilot-multi-auth
|
|
6
|
+
|
|
7
|
+
这个分支(fork)将旧的 GitHub Copilot chat-auth 流程替换为更新的 Copilot CLI 风格的 OAuth 流程,并使 `opencode` 使用您的账户实时获取的 Copilot 模型元数据。
|
|
8
|
+
|
|
9
|
+
## 如何使用
|
|
10
|
+
|
|
11
|
+
将该插件添加到您的 `opencode` 配置中:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"$schema": "https://opencode.ai/config.json",
|
|
16
|
+
"plugin": [
|
|
17
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.5"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
然后启动 `opencode` 并登录到 `github-copilot` 供应商。该插件处理类似 Copilot CLI 风格的设备授权流程,并在之后重用存储的 GitHub OAuth 令牌。
|
|
23
|
+
|
|
24
|
+
如果您还希望使用该包提供的 TUI 命令支持,请将相同的插件添加到您的 `tui.json` 或 `tui.jsonc` 中:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
29
|
+
"plugin": [
|
|
30
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.5"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
重新启动 OpenCode TUI 后,您可以打开命令面板并运行 `copilot-usage`,或直接输入 `/copilot-usage`。
|
|
36
|
+
插件会打开一个弹窗,显示从拆分的本地存储中加载的所有账户:
|
|
37
|
+
|
|
38
|
+
- OAuth/账户数据:`~/.local/share/opencode/copilot-auth.json`
|
|
39
|
+
- 单账户路由策略:`~/.config/opencode/copilot-auth.json`
|
|
40
|
+
|
|
41
|
+
对于发布前的本地开发,您可以直接加载该文件:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"$schema": "https://opencode.ai/config.json",
|
|
46
|
+
"plugin": [
|
|
47
|
+
"file:///absolute/path/to/dist/index.js"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
重要提示:如果文件路径包含 `opencode-copilot-auth`,当前的 `opencode` 构建可能会因为硬编码的插件名称过滤器而跳过加载它。请使用不包含该子字符串的路径。
|
|
53
|
+
|
|
54
|
+
如果您在开发过程中在本地加载插件并希望使用 `copilot-usage`,请同时将构建的 TUI 入口添加到您的 TUI 配置中:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
59
|
+
"plugin": [
|
|
60
|
+
"file:///absolute/path/to/dist/tui.js"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## `copilot-usage` 命令
|
|
66
|
+
|
|
67
|
+
此包现在包含一个名为 `copilot-usage` 的 TUI 命令。
|
|
68
|
+
|
|
69
|
+
- 命令面板入口:`copilot-usage`
|
|
70
|
+
- Slash 命令:`/copilot-usage`
|
|
71
|
+
|
|
72
|
+
它的功能:
|
|
73
|
+
|
|
74
|
+
- 从合并后的本地池中读取每个 Copilot 账户,池子组装自:
|
|
75
|
+
- `~/.local/share/opencode/copilot-auth.json` (OAuth/账户数据)
|
|
76
|
+
- `~/.config/opencode/copilot-auth.json` (路由策略)
|
|
77
|
+
- 使用该账户的 OAuth 令牌,为每个账户调用 GitHub Copilot 的配额端点 `/copilot_internal/user`
|
|
78
|
+
- 打开一个弹窗界面,为每个账户显示:
|
|
79
|
+
- 账户名称
|
|
80
|
+
- 已使用 / 总额度条
|
|
81
|
+
- 使用百分比
|
|
82
|
+
|
|
83
|
+
注意事项:
|
|
84
|
+
|
|
85
|
+
- 该命令是此包 TUI 目标的一部分,所以只配置 `opencode.json` 是不够的;`tui.json` 必须也加载此插件。
|
|
86
|
+
- 被禁用的账户仍然会显示在弹窗中,并会被标记为已禁用。
|
|
87
|
+
- 如果某个账户加载使用数据失败,弹窗仍会显示其他账户,并在行内包含该账户的错误消息。
|
|
88
|
+
|
|
89
|
+
## 这个分支有什么改变
|
|
90
|
+
|
|
91
|
+
- 授权流程:使用了类似 Copilot CLI 风格的 OAuth 客户端流程,并直接保留了 GitHub OAuth 令牌。
|
|
92
|
+
- 授权配额:获取 `/copilot_internal/user`,并使用授权提供的 Copilot API 基础 URL。
|
|
93
|
+
- 令牌交换:不再调用 `/copilot_internal/v2/token`。
|
|
94
|
+
- 请求配置:使用较新的 `copilot-developer-cli` 请求头,替代了旧版的聊天配置。
|
|
95
|
+
- 模型元数据:通过插件的 `provider.models` 钩子获取实时的 Copilot `/models` 响应,以便最终的 `opencode` 模型列表来源于 Copilot API 所支持的实时配额。
|
|
96
|
+
|
|
97
|
+
## 上下文窗口与模型限制
|
|
98
|
+
|
|
99
|
+
与上游的主要实际区别在于,此分支从 Copilot 动态应用实时的按模型限制,而不是仅依赖静态元数据。
|
|
100
|
+
|
|
101
|
+
这意味着 `opencode` 可以看到 Copilot 宣告的以下各项的值:
|
|
102
|
+
|
|
103
|
+
- `limit.context`
|
|
104
|
+
- `limit.input`
|
|
105
|
+
- `limit.output`
|
|
106
|
+
|
|
107
|
+
截至 2026 年 3 月 10 日,此分支使用的实时 GitHub Copilot `/models` 响应暴露了 Copilot CLI 的模型配置。下表比较了实时 Copilot CLI 的上下文窗口与 [`models.dev`](https://models.dev) 上的静态 `github-copilot` 目录。
|
|
108
|
+
|
|
109
|
+
| 模型 | 此分支 (CLI 上下文) | `models.dev` 上下文 | 差异 |
|
|
110
|
+
| ------------------- | ----------------------: | -------------------: | ---------: |
|
|
111
|
+
| `claude-opus-4.6` | 200,000 | 128,000 | +72,000 |
|
|
112
|
+
| `claude-sonnet-4.6` | 200,000 | 128,000 | +72,000 |
|
|
113
|
+
| `claude-haiku-4.5` | 144,000 | 128,000 | +16,000 |
|
|
114
|
+
|
|
115
|
+
实际收获是,该分支比静态的 `models.dev` 值提供了更大的实时 Claude 上下文窗口。
|
|
116
|
+
|
|
117
|
+
此分支观察到的例子:
|
|
118
|
+
|
|
119
|
+
- `claude-sonnet-4.6`
|
|
120
|
+
- context window (上下文窗口): `200000`
|
|
121
|
+
- prompt/input limit (提示/输入限制): `168000`
|
|
122
|
+
- output limit (输出限制): `32000`
|
|
123
|
+
- `claude-opus-4.6`
|
|
124
|
+
- context window (上下文窗口): `200000`
|
|
125
|
+
- prompt/input limit (提示/输入限制): `168000`
|
|
126
|
+
- output limit (输出限制): `64000`
|
|
127
|
+
- `claude-haiku-4.5`
|
|
128
|
+
- context window (上下文窗口): `144000`
|
|
129
|
+
- prompt/input limit (提示/输入限制): `128000`
|
|
130
|
+
- output limit (输出限制): `32000`
|
|
131
|
+
|
|
132
|
+
如果不打这些补丁,`opencode` 可能会因为其使用的初始静态模型目录而显示过期或偏小的限制。
|
|
133
|
+
|
|
134
|
+
## Claude 思考预算 (thinking budget) 行为
|
|
135
|
+
|
|
136
|
+
此分支还改变了 Copilot Claude 请求的行为:
|
|
137
|
+
|
|
138
|
+
- 当选择 `thinking` 变体时,它发送 `thinking_budget: 16000`
|
|
139
|
+
- 当未选择变体时,它完全省略 `thinking_budget`
|
|
140
|
+
|
|
141
|
+
这不同于上游的 `opencode`,上游目前为内置的 `thinking` 变体发送 `thinking_budget: 4000`。
|
|
142
|
+
|
|
143
|
+
该插件故意不尝试修改 `opencode` 的核心 UI。所以可见的 Claude 变体列表依然由 `opencode` 本身控制;该分支改变的是请求行为,而不是内置变体选择器的标签。
|
|
144
|
+
|
|
145
|
+
## 发布 (Publishing)
|
|
146
|
+
|
|
147
|
+
```zsh
|
|
148
|
+
./script/publish.ts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 多账户支持
|
|
152
|
+
|
|
153
|
+
此分支在分离的本地文件中保存 Copilot 的账户状态:
|
|
154
|
+
|
|
155
|
+
- OAuth 必需的账户数据:`~/.local/share/opencode/copilot-auth.json`
|
|
156
|
+
- 单账户路由策略:`~/.config/opencode/copilot-auth.json`
|
|
157
|
+
|
|
158
|
+
在启动时,该插件会自动将旧版的单文件存储迁移到这种双文件布局中。
|
|
159
|
+
|
|
160
|
+
每次成功的 OAuth 登录都会自动更新该池:用一个具有特定作用域部署的新账户登录会追加一条新记录,而在同一部署上用相同的 GitHub 身份再次登录会更新现有记录,而不是创建重复项。
|
|
161
|
+
|
|
162
|
+
在运行时,插件将这两个文件合并为路由所用的同一内存中 `version: 2` 池结构 (`accounts`)。
|
|
163
|
+
|
|
164
|
+
| 字段 | 含义 |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `id` | 与账户记录一起存储的稳定的人类友好标识符。 |
|
|
167
|
+
| `name` | 账户的显示名称。 |
|
|
168
|
+
| `enabled` | 该账户是否能参与自动路由。被禁用的账户依然会保存,但在挑选赢家时会被忽略。 |
|
|
169
|
+
| `priority` | 当多个已启用的账户都可以提供同一个原始模型 ID 的服务时,较低的值优先(即数字越小优先级越高)。 |
|
|
170
|
+
| `allowlist` | 此账户获准提供服务的原始模型 ID 或 `*` 通配符模式。如果不为空,则账户只能提供与这些条目之一匹配的模型。 |
|
|
171
|
+
| `blocklist` | 此账户绝对不能提供服务的原始模型 ID 或 `*` 通配符模式。如果 `allowlist` 和 `blocklist` 均不为空,插件会首先检查 `allowlist`,然后应用 `blocklist`。 |
|
|
172
|
+
|
|
173
|
+
自动路由是基于 `opencode` 已经使用的原始 Copilot 模型 ID 来工作的。插件根据 `enabled` 过滤合格的账户,然后首先检查 `allowlist`(当不为空时,模型必须匹配其精确条目或通配符模式之一),然后应用 `blocklist`,最后通过最低的 `priority` 挑选出唯一的一个获胜账户(带有一个稳定的基于 key 的平局决胜机制)。模型 ID 本身没有被重写,因此账户标识不会出现在模型 ID 中。
|
|
174
|
+
|
|
175
|
+
通配符匹配区分大小写,且目前支持用 `*` 表示模式中任意位置的零个或多个字符。例如,`claude-*` 可以匹配 `claude-sonnet-4.6` 和 `claude-opus-4.6`。
|
|
176
|
+
|
|
177
|
+
示例授权文件 (`~/.local/share/opencode/copilot-auth.json`):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"version": 2,
|
|
182
|
+
"accounts": [
|
|
183
|
+
{
|
|
184
|
+
"key": "github.com:12345678",
|
|
185
|
+
"deployment": "github.com",
|
|
186
|
+
"domain": "github.com",
|
|
187
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
188
|
+
"identity": {
|
|
189
|
+
"login": "octocat",
|
|
190
|
+
"userId": 12345678
|
|
191
|
+
},
|
|
192
|
+
"auth": {
|
|
193
|
+
"type": "oauth",
|
|
194
|
+
"refresh": "<oauth token>"
|
|
195
|
+
},
|
|
196
|
+
"createdAt": "2026-04-15T00:00:00.000Z",
|
|
197
|
+
"updatedAt": "2026-04-15T00:00:00.000Z"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
示例策略文件 (`~/.config/opencode/copilot-auth.json`):
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"version": 2,
|
|
208
|
+
"accounts": [
|
|
209
|
+
{
|
|
210
|
+
"key": "github.com:12345678",
|
|
211
|
+
"enabled": true,
|
|
212
|
+
"priority": 100,
|
|
213
|
+
"allowlist": ["claude-*"],
|
|
214
|
+
"blocklist": []
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
```
|
package/dist/auth.d.ts
CHANGED
|
@@ -18,3 +18,4 @@ export declare function buildHeaders(init: RequestInit | undefined, copilotToken
|
|
|
18
18
|
export declare function resolveSelectedPoolAccount(pool: AccountPool, accountKey?: string, requestedRawModelId?: string): PoolAccount | null;
|
|
19
19
|
export declare function refreshSelectedAccountBaseURL(accountKey: string): Promise<PoolAccount>;
|
|
20
20
|
export declare function fetchWithSelectedAccount(input: RequestInfo | URL, init: RequestInit | undefined, selectedAccount: PoolAccount): Promise<Response>;
|
|
21
|
+
export declare function fetchWithAccountFallback(input: RequestInfo | URL, init: RequestInit | undefined, candidates: PoolAccount[]): Promise<Response>;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { API_VERSION, COPILOT_TOKEN_EXPIRY_BUFFER_MS, DEFAULT_COPILOT_BASE_URL, VSCODE_HEADERS, } from "./constants.js";
|
|
2
2
|
import { getSelectedAccountExpiredError, getSelectedAccountMissingError } from "./errors.js";
|
|
3
|
-
import { readPool, resolveWinnerAccount,
|
|
3
|
+
import { readPool, resolveWinnerAccount, writePoolAuthData } from "./pool.js";
|
|
4
4
|
import { stripRoutingHeaders } from "./routing.js";
|
|
5
5
|
import { applyBaseURLToRequestInput, getConversationMetadata, isValidBaseURL, normalizeDomain, normalizeHeaderObject, } from "./utils.js";
|
|
6
6
|
export function getUrls(domain) {
|
|
@@ -208,7 +208,7 @@ export async function refreshSelectedAccountBaseURL(accountKey) {
|
|
|
208
208
|
}
|
|
209
209
|
: account),
|
|
210
210
|
};
|
|
211
|
-
|
|
211
|
+
writePoolAuthData(updatedPool);
|
|
212
212
|
return updatedPool.accounts[accountIndex];
|
|
213
213
|
}
|
|
214
214
|
export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
@@ -237,3 +237,19 @@ export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
|
237
237
|
}
|
|
238
238
|
return retryResponse;
|
|
239
239
|
}
|
|
240
|
+
export async function fetchWithAccountFallback(input, init, candidates) {
|
|
241
|
+
if (candidates.length === 0) {
|
|
242
|
+
throw new Error("[opencode-copilot-cli-auth] No eligible accounts available for this model; re-login required");
|
|
243
|
+
}
|
|
244
|
+
let lastError;
|
|
245
|
+
for (const account of candidates) {
|
|
246
|
+
try {
|
|
247
|
+
return await fetchWithSelectedAccount(input, init, account);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
lastError = error;
|
|
251
|
+
console.warn(`[opencode-copilot-cli-auth] Account ${account.key ?? "unknown"} failed, trying next:`, error instanceof Error ? error.message : String(error));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw lastError ?? new Error("[opencode-copilot-cli-auth] All candidate accounts exhausted; re-login required");
|
|
255
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const ACCOUNT_POOL_SCHEMA_VERSION =
|
|
1
|
+
export declare const ACCOUNT_POOL_SCHEMA_VERSION = 2;
|
|
2
2
|
export declare const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
3
3
|
export declare const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
4
4
|
export declare const INTERNAL_ROUTING_HEADERS: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const ACCOUNT_POOL_SCHEMA_VERSION =
|
|
1
|
+
export const ACCOUNT_POOL_SCHEMA_VERSION = 2;
|
|
2
2
|
export const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
3
3
|
export const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
4
4
|
export const INTERNAL_ROUTING_HEADERS = new Set([
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { lookupGitHubIdentity } from "./auth.js";
|
|
3
|
-
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool } from "./pool.js";
|
|
3
|
+
import { deriveAccountKey, getPolicyPath, getPoolPath, migrateLegacyPoolStorageIfNeeded, readPool, resolveCandidateAccounts, resolveWinnerAccount, upsertAccount, writePool } from "./pool.js";
|
|
4
4
|
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
5
|
-
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
5
|
+
export { getPoolPath, getPolicyPath, readPool, writePool, migrateLegacyPoolStorageIfNeeded, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveCandidateAccounts, resolveWinnerAccount, };
|
|
6
6
|
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
7
7
|
export declare const CopilotAuthPlugin: Plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { CLIENT_ID, OAUTH_POLLING_SAFETY_MARGIN_MS, OAUTH_SCOPES, ROUTING_ACCOUNT_KEY_HEADER, ROUTING_SOURCE_HEADER, } from "./constants.js";
|
|
2
|
-
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getCopilotToken, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
2
|
+
import { buildHeaders, fetchEntitlement, fetchWithAccountFallback, fetchWithSelectedAccount, getBaseURL, getCopilotToken, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
3
3
|
import { buildPoolBackedModels, normalizeExistingModels, resolveProviderModels } from "./models.js";
|
|
4
|
-
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
4
|
+
import { deriveAccountKey, getPolicyPath, getPoolPath, migrateLegacyPoolStorageIfNeeded, readPool, resolveCandidateAccounts, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
5
5
|
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
6
6
|
import { applyBaseURLToRequestInput, getHeader, getConversationMetadata, getRequestedRawModelId, normalizeDomain, resolveClaudeThinkingBudget, } from "./utils.js";
|
|
7
|
-
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
7
|
+
export { getPoolPath, getPolicyPath, readPool, writePool, migrateLegacyPoolStorageIfNeeded, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveCandidateAccounts, resolveWinnerAccount, };
|
|
8
8
|
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
9
9
|
export const CopilotAuthPlugin = async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
migrateLegacyPoolStorageIfNeeded();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.warn("[opencode-copilot-cli-auth] Failed to run pool storage migration:", error instanceof Error ? error.message : String(error));
|
|
15
|
+
}
|
|
10
16
|
return {
|
|
11
17
|
provider: {
|
|
12
18
|
id: "github-copilot",
|
|
@@ -59,6 +65,15 @@ export const CopilotAuthPlugin = async (input) => {
|
|
|
59
65
|
const accountKey = getHeader(init?.headers, ROUTING_ACCOUNT_KEY_HEADER);
|
|
60
66
|
const requestedRawModelId = getRequestedRawModelId(init);
|
|
61
67
|
if (pool.accounts.length > 0) {
|
|
68
|
+
if (requestedRawModelId) {
|
|
69
|
+
const candidates = resolveCandidateAccounts(requestedRawModelId, pool);
|
|
70
|
+
const oauthCandidates = candidates.filter((account) => account.auth?.type === "oauth"
|
|
71
|
+
&& typeof account.auth.refresh === "string"
|
|
72
|
+
&& account.auth.refresh.trim());
|
|
73
|
+
if (oauthCandidates.length > 0) {
|
|
74
|
+
return fetchWithAccountFallback(inputRequest, init, oauthCandidates);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
62
77
|
const selectedAccount = resolveSelectedPoolAccount(pool, accountKey, requestedRawModelId);
|
|
63
78
|
if (selectedAccount?.auth?.type === "oauth") {
|
|
64
79
|
return fetchWithSelectedAccount(inputRequest, init, selectedAccount);
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { AccountPool, PoolAccount, UpsertAccountData } from "./types.js";
|
|
2
2
|
export declare function getPoolPath(): string;
|
|
3
|
+
export declare function getPolicyPath(): string;
|
|
3
4
|
export declare function validatePoolSchema(pool: unknown, context: string): AccountPool;
|
|
4
5
|
export declare function readPool(): AccountPool;
|
|
5
6
|
export declare function writePool(pool: AccountPool): void;
|
|
7
|
+
export declare function writePoolAuthData(pool: AccountPool): void;
|
|
8
|
+
export declare function migrateLegacyPoolStorageIfNeeded(): void;
|
|
6
9
|
export declare function deriveAccountKey(deployment: string, userId: number): string;
|
|
7
10
|
export declare function upsertAccount(pool: AccountPool, accountData: UpsertAccountData): AccountPool;
|
|
11
|
+
export declare function resolveCandidateAccounts(rawModelId: string, pool: AccountPool): PoolAccount[];
|
|
8
12
|
export declare function resolveWinnerAccount(rawModelId: string, pool: AccountPool): PoolAccount;
|
package/dist/pool.js
CHANGED
|
@@ -6,44 +6,344 @@ import { matchesAnyModelIdPattern, normalizeDomain, normalizeIdSource, normalize
|
|
|
6
6
|
export function getPoolPath() {
|
|
7
7
|
return `${homedir()}/.local/share/opencode/copilot-auth.json`;
|
|
8
8
|
}
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
export function getPolicyPath() {
|
|
10
|
+
return `${homedir()}/.config/opencode/copilot-auth.json`;
|
|
11
|
+
}
|
|
12
|
+
function assertObject(value, context) {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected object.`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function assertString(value, context) {
|
|
19
|
+
if (typeof value !== "string") {
|
|
20
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected string.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function assertNumber(value, context) {
|
|
25
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
26
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected finite number.`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function assertBoolean(value, context) {
|
|
31
|
+
if (typeof value !== "boolean") {
|
|
32
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected boolean.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function assertNullableString(value, context) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return assertString(value, context);
|
|
41
|
+
}
|
|
42
|
+
function assertStringArray(value, context) {
|
|
43
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
44
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected string[].`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function parsePoolIdentity(value, context) {
|
|
49
|
+
const identityObject = assertObject(value, context);
|
|
50
|
+
return {
|
|
51
|
+
login: assertString(identityObject.login, `${context}.login`),
|
|
52
|
+
userId: assertNumber(identityObject.userId, `${context}.userId`),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseOAuthAuth(value, context) {
|
|
56
|
+
const authObject = assertObject(value, context);
|
|
57
|
+
if (authObject.type !== "oauth") {
|
|
58
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}.type: expected \"oauth\".`);
|
|
59
|
+
}
|
|
60
|
+
const parsed = {
|
|
61
|
+
type: "oauth",
|
|
62
|
+
refresh: assertString(authObject.refresh, `${context}.refresh`),
|
|
63
|
+
};
|
|
64
|
+
if (typeof authObject.access === "string") {
|
|
65
|
+
parsed.access = authObject.access;
|
|
66
|
+
}
|
|
67
|
+
if (typeof authObject.expires === "number" && Number.isFinite(authObject.expires)) {
|
|
68
|
+
parsed.expires = authObject.expires;
|
|
69
|
+
}
|
|
70
|
+
if (authObject.baseUrl === null || typeof authObject.baseUrl === "string") {
|
|
71
|
+
parsed.baseUrl = authObject.baseUrl;
|
|
72
|
+
}
|
|
73
|
+
if (typeof authObject.provider === "string") {
|
|
74
|
+
parsed.provider = authObject.provider;
|
|
75
|
+
}
|
|
76
|
+
if (typeof authObject.enterpriseUrl === "string") {
|
|
77
|
+
parsed.enterpriseUrl = authObject.enterpriseUrl;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
function parseAuthPoolAccount(value, context) {
|
|
82
|
+
const account = assertObject(value, context);
|
|
83
|
+
return {
|
|
84
|
+
key: assertString(account.key, `${context}.key`),
|
|
85
|
+
deployment: assertString(account.deployment, `${context}.deployment`),
|
|
86
|
+
domain: assertString(account.domain, `${context}.domain`),
|
|
87
|
+
identity: parsePoolIdentity(account.identity, `${context}.identity`),
|
|
88
|
+
enterpriseUrl: assertNullableString(account.enterpriseUrl, `${context}.enterpriseUrl`),
|
|
89
|
+
baseUrl: assertNullableString(account.baseUrl, `${context}.baseUrl`),
|
|
90
|
+
auth: parseOAuthAuth(account.auth, `${context}.auth`),
|
|
91
|
+
createdAt: assertString(account.createdAt, `${context}.createdAt`),
|
|
92
|
+
updatedAt: assertString(account.updatedAt, `${context}.updatedAt`),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parsePolicyPoolAccount(value, context) {
|
|
96
|
+
const account = assertObject(value, context);
|
|
97
|
+
return {
|
|
98
|
+
key: assertString(account.key, `${context}.key`),
|
|
99
|
+
enabled: assertBoolean(account.enabled, `${context}.enabled`),
|
|
100
|
+
priority: assertNumber(account.priority, `${context}.priority`),
|
|
101
|
+
allowlist: assertStringArray(account.allowlist, `${context}.allowlist`),
|
|
102
|
+
blocklist: assertStringArray(account.blocklist, `${context}.blocklist`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseVersionedAccountsDocument(value, context) {
|
|
106
|
+
const document = assertObject(value, context);
|
|
107
|
+
if (document.version !== ACCOUNT_POOL_SCHEMA_VERSION || !Array.isArray(document.accounts)) {
|
|
14
108
|
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected { version: ${ACCOUNT_POOL_SCHEMA_VERSION}, accounts: [] } schema.`);
|
|
15
109
|
}
|
|
16
|
-
return
|
|
110
|
+
return {
|
|
111
|
+
version: document.version,
|
|
112
|
+
accounts: document.accounts,
|
|
113
|
+
};
|
|
17
114
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
23
|
-
accounts: [],
|
|
24
|
-
};
|
|
25
|
-
writePool(defaultPool);
|
|
26
|
-
return defaultPool;
|
|
115
|
+
function parseLegacyVersionedAccountsDocument(value, context) {
|
|
116
|
+
const document = assertObject(value, context);
|
|
117
|
+
if (document.version !== 1 || !Array.isArray(document.accounts)) {
|
|
118
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected legacy { version: 1, accounts: [] } schema.`);
|
|
27
119
|
}
|
|
28
|
-
|
|
29
|
-
|
|
120
|
+
return {
|
|
121
|
+
version: document.version,
|
|
122
|
+
accounts: document.accounts,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function validateAuthPoolSchema(pool, context) {
|
|
126
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
127
|
+
return {
|
|
128
|
+
version: parsed.version,
|
|
129
|
+
accounts: parsed.accounts.map((account, index) => parseAuthPoolAccount(account, `${context}.accounts[${index}]`)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function validatePolicyPoolSchema(pool, context) {
|
|
133
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
134
|
+
return {
|
|
135
|
+
version: parsed.version,
|
|
136
|
+
accounts: parsed.accounts.map((account, index) => parsePolicyPoolAccount(account, `${context}.accounts[${index}]`)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function validateLegacyPoolSchema(pool, context) {
|
|
140
|
+
const parsed = parseLegacyVersionedAccountsDocument(pool, context);
|
|
141
|
+
return {
|
|
142
|
+
version: parsed.version,
|
|
143
|
+
accounts: parsed.accounts.map((account, index) => {
|
|
144
|
+
const accountObject = assertObject(account, `${context}.accounts[${index}]`);
|
|
145
|
+
return {
|
|
146
|
+
key: assertString(accountObject.key, `${context}.accounts[${index}].key`),
|
|
147
|
+
id: assertString(accountObject.id, `${context}.accounts[${index}].id`),
|
|
148
|
+
name: assertString(accountObject.name, `${context}.accounts[${index}].name`),
|
|
149
|
+
enabled: assertBoolean(accountObject.enabled, `${context}.accounts[${index}].enabled`),
|
|
150
|
+
priority: assertNumber(accountObject.priority, `${context}.accounts[${index}].priority`),
|
|
151
|
+
deployment: assertString(accountObject.deployment, `${context}.accounts[${index}].deployment`),
|
|
152
|
+
domain: assertString(accountObject.domain, `${context}.accounts[${index}].domain`),
|
|
153
|
+
identity: parsePoolIdentity(accountObject.identity, `${context}.accounts[${index}].identity`),
|
|
154
|
+
enterpriseUrl: assertNullableString(accountObject.enterpriseUrl, `${context}.accounts[${index}].enterpriseUrl`),
|
|
155
|
+
baseUrl: assertNullableString(accountObject.baseUrl, `${context}.accounts[${index}].baseUrl`),
|
|
156
|
+
allowlist: assertStringArray(accountObject.allowlist, `${context}.accounts[${index}].allowlist`),
|
|
157
|
+
blocklist: assertStringArray(accountObject.blocklist, `${context}.accounts[${index}].blocklist`),
|
|
158
|
+
auth: parseOAuthAuth(accountObject.auth, `${context}.accounts[${index}].auth`),
|
|
159
|
+
createdAt: assertString(accountObject.createdAt, `${context}.accounts[${index}].createdAt`),
|
|
160
|
+
updatedAt: assertString(accountObject.updatedAt, `${context}.accounts[${index}].updatedAt`),
|
|
161
|
+
};
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toDefaultPolicyAccount(account) {
|
|
166
|
+
return {
|
|
167
|
+
key: account.key,
|
|
168
|
+
enabled: true,
|
|
169
|
+
priority: 0,
|
|
170
|
+
allowlist: [],
|
|
171
|
+
blocklist: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function parseJsonFile(filePath, context) {
|
|
175
|
+
const raw = readFileSync(filePath, "utf8");
|
|
30
176
|
try {
|
|
31
|
-
|
|
177
|
+
return JSON.parse(raw);
|
|
32
178
|
}
|
|
33
179
|
catch (error) {
|
|
34
|
-
throw new Error(`[opencode-copilot-cli-auth] Malformed JSON in
|
|
180
|
+
throw new Error(`[opencode-copilot-cli-auth] Malformed JSON in ${context} at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
181
|
}
|
|
36
|
-
|
|
182
|
+
}
|
|
183
|
+
function writeJsonAtomic(filePath, payload) {
|
|
184
|
+
const directoryPath = dirname(filePath);
|
|
185
|
+
const tmpPath = `${filePath}.tmp`;
|
|
186
|
+
mkdirSync(directoryPath, { recursive: true });
|
|
187
|
+
writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
188
|
+
renameSync(tmpPath, filePath);
|
|
189
|
+
chmodSync(filePath, 0o600);
|
|
190
|
+
}
|
|
191
|
+
function buildAuthPoolDocument(pool) {
|
|
192
|
+
return {
|
|
193
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
194
|
+
accounts: pool.accounts.map((account) => ({
|
|
195
|
+
key: account.key,
|
|
196
|
+
deployment: account.deployment,
|
|
197
|
+
domain: account.domain,
|
|
198
|
+
identity: account.identity,
|
|
199
|
+
enterpriseUrl: account.enterpriseUrl,
|
|
200
|
+
baseUrl: account.baseUrl,
|
|
201
|
+
auth: account.auth,
|
|
202
|
+
createdAt: account.createdAt,
|
|
203
|
+
updatedAt: account.updatedAt,
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function buildPolicyPoolDocument(pool) {
|
|
208
|
+
return {
|
|
209
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
210
|
+
accounts: pool.accounts.map((account) => ({
|
|
211
|
+
key: account.key,
|
|
212
|
+
enabled: account.enabled,
|
|
213
|
+
priority: account.priority,
|
|
214
|
+
allowlist: account.allowlist,
|
|
215
|
+
blocklist: account.blocklist,
|
|
216
|
+
})),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function mergePoolDocuments(authPool, policyPool) {
|
|
220
|
+
const policyByKey = new Map(policyPool.accounts.map((account) => [account.key, account]));
|
|
221
|
+
const merged = {
|
|
222
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
223
|
+
accounts: authPool.accounts.map((authAccount) => {
|
|
224
|
+
const policy = policyByKey.get(authAccount.key) ?? toDefaultPolicyAccount(authAccount);
|
|
225
|
+
const userIdText = String(authAccount.identity.userId);
|
|
226
|
+
const fallbackId = normalizeIdSource(authAccount.identity.login || userIdText) || userIdText;
|
|
227
|
+
const fallbackName = authAccount.identity.login || userIdText;
|
|
228
|
+
return {
|
|
229
|
+
key: authAccount.key,
|
|
230
|
+
id: fallbackId,
|
|
231
|
+
name: fallbackName,
|
|
232
|
+
enabled: policy.enabled,
|
|
233
|
+
priority: policy.priority,
|
|
234
|
+
deployment: authAccount.deployment,
|
|
235
|
+
domain: authAccount.domain,
|
|
236
|
+
identity: authAccount.identity,
|
|
237
|
+
enterpriseUrl: authAccount.enterpriseUrl,
|
|
238
|
+
baseUrl: authAccount.baseUrl,
|
|
239
|
+
allowlist: policy.allowlist,
|
|
240
|
+
blocklist: policy.blocklist,
|
|
241
|
+
auth: authAccount.auth,
|
|
242
|
+
createdAt: authAccount.createdAt,
|
|
243
|
+
updatedAt: authAccount.updatedAt,
|
|
244
|
+
};
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
return validatePoolSchema(merged, "merged account pool");
|
|
248
|
+
}
|
|
249
|
+
export function validatePoolSchema(pool, context) {
|
|
250
|
+
const parsed = parseVersionedAccountsDocument(pool, context);
|
|
251
|
+
return {
|
|
252
|
+
version: parsed.version,
|
|
253
|
+
accounts: parsed.accounts.map((account, index) => {
|
|
254
|
+
const accountObject = assertObject(account, `${context}.accounts[${index}]`);
|
|
255
|
+
return {
|
|
256
|
+
key: assertString(accountObject.key, `${context}.accounts[${index}].key`),
|
|
257
|
+
id: assertString(accountObject.id, `${context}.accounts[${index}].id`),
|
|
258
|
+
name: assertString(accountObject.name, `${context}.accounts[${index}].name`),
|
|
259
|
+
enabled: assertBoolean(accountObject.enabled, `${context}.accounts[${index}].enabled`),
|
|
260
|
+
priority: assertNumber(accountObject.priority, `${context}.accounts[${index}].priority`),
|
|
261
|
+
deployment: assertString(accountObject.deployment, `${context}.accounts[${index}].deployment`),
|
|
262
|
+
domain: assertString(accountObject.domain, `${context}.accounts[${index}].domain`),
|
|
263
|
+
identity: parsePoolIdentity(accountObject.identity, `${context}.accounts[${index}].identity`),
|
|
264
|
+
enterpriseUrl: assertNullableString(accountObject.enterpriseUrl, `${context}.accounts[${index}].enterpriseUrl`),
|
|
265
|
+
baseUrl: assertNullableString(accountObject.baseUrl, `${context}.accounts[${index}].baseUrl`),
|
|
266
|
+
allowlist: assertStringArray(accountObject.allowlist, `${context}.accounts[${index}].allowlist`),
|
|
267
|
+
blocklist: assertStringArray(accountObject.blocklist, `${context}.accounts[${index}].blocklist`),
|
|
268
|
+
auth: parseOAuthAuth(accountObject.auth, `${context}.accounts[${index}].auth`),
|
|
269
|
+
createdAt: assertString(accountObject.createdAt, `${context}.accounts[${index}].createdAt`),
|
|
270
|
+
updatedAt: assertString(accountObject.updatedAt, `${context}.accounts[${index}].updatedAt`),
|
|
271
|
+
};
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function readPool() {
|
|
276
|
+
const authPath = getPoolPath();
|
|
277
|
+
const policyPath = getPolicyPath();
|
|
278
|
+
const defaultDocument = {
|
|
279
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
280
|
+
accounts: [],
|
|
281
|
+
};
|
|
282
|
+
if (!existsSync(authPath)) {
|
|
283
|
+
writeJsonAtomic(authPath, defaultDocument);
|
|
284
|
+
}
|
|
285
|
+
if (!existsSync(policyPath)) {
|
|
286
|
+
writeJsonAtomic(policyPath, defaultDocument);
|
|
287
|
+
}
|
|
288
|
+
const parsedAuth = parseJsonFile(authPath, "Copilot auth file");
|
|
289
|
+
const parsedPolicy = parseJsonFile(policyPath, "Copilot routing policy file");
|
|
290
|
+
const authPool = validateAuthPoolSchema(parsedAuth, `auth pool file at ${authPath}`);
|
|
291
|
+
const policyPool = validatePolicyPoolSchema(parsedPolicy, `policy pool file at ${policyPath}`);
|
|
292
|
+
return mergePoolDocuments(authPool, policyPool);
|
|
37
293
|
}
|
|
38
294
|
export function writePool(pool) {
|
|
39
295
|
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
296
|
+
const authDocument = validateAuthPoolSchema(buildAuthPoolDocument(validatedPool), "auth pool payload");
|
|
297
|
+
const policyDocument = validatePolicyPoolSchema(buildPolicyPoolDocument(validatedPool), "policy pool payload");
|
|
298
|
+
writeJsonAtomic(getPolicyPath(), policyDocument);
|
|
299
|
+
writeJsonAtomic(getPoolPath(), authDocument);
|
|
300
|
+
}
|
|
301
|
+
export function writePoolAuthData(pool) {
|
|
302
|
+
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
303
|
+
const authDocument = validateAuthPoolSchema(buildAuthPoolDocument(validatedPool), "auth pool payload");
|
|
304
|
+
writeJsonAtomic(getPoolPath(), authDocument);
|
|
305
|
+
}
|
|
306
|
+
export function migrateLegacyPoolStorageIfNeeded() {
|
|
307
|
+
const authPath = getPoolPath();
|
|
308
|
+
const policyPath = getPolicyPath();
|
|
309
|
+
if (!existsSync(authPath)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const parsed = parseJsonFile(authPath, "Copilot account pool file");
|
|
313
|
+
try {
|
|
314
|
+
const legacyPool = validateLegacyPoolSchema(parsed, `legacy account pool file at ${authPath}`);
|
|
315
|
+
const existingPolicy = existsSync(policyPath)
|
|
316
|
+
? validatePolicyPoolSchema(parseJsonFile(policyPath, "Copilot routing policy file"), `policy pool file at ${policyPath}`)
|
|
317
|
+
: null;
|
|
318
|
+
const migratedPolicy = {
|
|
319
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
320
|
+
accounts: legacyPool.accounts.map((account) => {
|
|
321
|
+
const existingPolicyAccount = existingPolicy?.accounts.find((candidate) => candidate.key === account.key);
|
|
322
|
+
return existingPolicyAccount ?? {
|
|
323
|
+
key: account.key,
|
|
324
|
+
enabled: account.enabled,
|
|
325
|
+
priority: account.priority,
|
|
326
|
+
allowlist: account.allowlist,
|
|
327
|
+
blocklist: account.blocklist,
|
|
328
|
+
};
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
writeJsonAtomic(policyPath, validatePolicyPoolSchema(migratedPolicy, "migrated policy pool payload"));
|
|
332
|
+
writeJsonAtomic(authPath, validateAuthPoolSchema(buildAuthPoolDocument(legacyPool), "migrated auth pool payload"));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Intentionally continue: file may already be auth-only format or a non-legacy document.
|
|
337
|
+
}
|
|
338
|
+
if (existsSync(policyPath)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const authPool = validateAuthPoolSchema(parsed, `auth pool file at ${authPath}`);
|
|
342
|
+
const defaultPolicyPool = {
|
|
343
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
344
|
+
accounts: authPool.accounts.map((account) => toDefaultPolicyAccount(account)),
|
|
345
|
+
};
|
|
346
|
+
writeJsonAtomic(policyPath, validatePolicyPoolSchema(defaultPolicyPool, "default policy pool payload"));
|
|
47
347
|
}
|
|
48
348
|
function deriveDefaultAccountId(accounts, key, identity) {
|
|
49
349
|
const userIdText = String(identity.userId);
|
|
@@ -134,7 +434,7 @@ export function upsertAccount(pool, accountData) {
|
|
|
134
434
|
accounts: nextAccounts,
|
|
135
435
|
};
|
|
136
436
|
}
|
|
137
|
-
export function
|
|
437
|
+
export function resolveCandidateAccounts(rawModelId, pool) {
|
|
138
438
|
const canAccountServeModel = (account) => {
|
|
139
439
|
const allowlist = normalizeList(account?.allowlist);
|
|
140
440
|
if (allowlist.length > 0 && !matchesAnyModelIdPattern(allowlist, rawModelId)) {
|
|
@@ -143,7 +443,7 @@ export function resolveWinnerAccount(rawModelId, pool) {
|
|
|
143
443
|
const blocklist = normalizeList(account?.blocklist);
|
|
144
444
|
return !matchesAnyModelIdPattern(blocklist, rawModelId);
|
|
145
445
|
};
|
|
146
|
-
|
|
446
|
+
return (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
147
447
|
.filter((account) => account?.enabled !== false)
|
|
148
448
|
.filter(canAccountServeModel)
|
|
149
449
|
.sort((left, right) => {
|
|
@@ -153,5 +453,7 @@ export function resolveWinnerAccount(rawModelId, pool) {
|
|
|
153
453
|
}
|
|
154
454
|
return String(left?.key ?? "").localeCompare(String(right?.key ?? ""));
|
|
155
455
|
});
|
|
156
|
-
|
|
456
|
+
}
|
|
457
|
+
export function resolveWinnerAccount(rawModelId, pool) {
|
|
458
|
+
return resolveCandidateAccounts(rawModelId, pool)[0] ?? null;
|
|
157
459
|
}
|
package/dist/tui.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,24 @@ export interface PoolAccount {
|
|
|
28
28
|
createdAt: string;
|
|
29
29
|
updatedAt: string;
|
|
30
30
|
}
|
|
31
|
+
export interface AuthPoolAccount {
|
|
32
|
+
key: string;
|
|
33
|
+
deployment: string;
|
|
34
|
+
domain: string;
|
|
35
|
+
identity: PoolIdentity;
|
|
36
|
+
enterpriseUrl: string | null;
|
|
37
|
+
baseUrl: string | null;
|
|
38
|
+
auth: OAuthAuth;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PolicyPoolAccount {
|
|
43
|
+
key: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
priority: number;
|
|
46
|
+
allowlist: string[];
|
|
47
|
+
blocklist: string[];
|
|
48
|
+
}
|
|
31
49
|
export interface UpsertAccountData {
|
|
32
50
|
key: string;
|
|
33
51
|
deployment?: string;
|
|
@@ -42,6 +60,14 @@ export interface AccountPool {
|
|
|
42
60
|
version: number;
|
|
43
61
|
accounts: PoolAccount[];
|
|
44
62
|
}
|
|
63
|
+
export interface AuthPoolDocument {
|
|
64
|
+
version: number;
|
|
65
|
+
accounts: AuthPoolAccount[];
|
|
66
|
+
}
|
|
67
|
+
export interface PolicyPoolDocument {
|
|
68
|
+
version: number;
|
|
69
|
+
accounts: PolicyPoolAccount[];
|
|
70
|
+
}
|
|
45
71
|
export interface LiveModel {
|
|
46
72
|
id: string;
|
|
47
73
|
name?: string;
|
package/dist/usage.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function getCopilotUsageDialogMessage() {
|
|
|
106
106
|
const pool = readPool();
|
|
107
107
|
const accounts = [...pool.accounts].sort((left, right) => left.key.localeCompare(right.key));
|
|
108
108
|
if (accounts.length === 0) {
|
|
109
|
-
throw new Error("No Copilot accounts found in the account pool. Log in again to populate ~/.local/share/opencode/copilot-auth.json.");
|
|
109
|
+
throw new Error("No Copilot accounts found in the account pool. Log in again to populate ~/.local/share/opencode/copilot-auth.json (auth) and ~/.config/opencode/copilot-auth.json (policy).");
|
|
110
110
|
}
|
|
111
111
|
const sections = await Promise.all(accounts.map((account) => getCopilotUsageSummary(account)));
|
|
112
112
|
return sections.join("\n\n");
|
package/index.mjs
CHANGED