opencode-claude-code-ua 0.0.0-stage → 0.3.6

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 ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magpie Codex Profile contributors
4
+ Copyright (c) 2026 sub2api-magpie-plugin contributors
5
+ Copyright (c) 2026 Claude Code UA contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,9 @@
1
+ # 第三方代码说明
2
+
3
+ UA PRO 按 MIT 许可分发,原始版权声明保留在 [LICENSE](LICENSE)。
4
+
5
+ - Profile 配置、设置服务、New API 账号与调度以及部分界面基于 `magpie-codex-profile` 0.7.0,按 MIT 许可复用和适配。原始作者:Magpie Codex Profile contributors。
6
+ - 初始 Codex / Claude 转换模板及部分兼容算法参考并适配 MIT 项目 [FischelChern/sub2api-magpie-plugin](https://github.com/FischelChern/sub2api-magpie-plugin) 0.4.16,固定提交 `a22d079b241e630fc05d4ed2890580156c7828dd`。
7
+ - 未包含参考项目的用户配置、凭据、抓包、截图或其他运行数据。
8
+
9
+ 兼容模板用于已获授权的上游 API,不提供订阅 OAuth、设备认证、账单签名或额外访问权限。
package/README.md CHANGED
@@ -1,3 +1,197 @@
1
- # Temporary Holding Version
1
+ # UA PRO
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Magpie 的出站客户端兼容插件。将 **Codex / Claude Code 请求规则**保存为版本化模板,按连接或模型应用,支持 Responses、Anthropic Messages、Chat Completions 和 Gemini。
4
+
5
+ 当前版本 **0.3.6**,npm 包名 [`opencode-claude-code-ua`](https://www.npmjs.com/package/opencode-claude-code-ua),主供应商 ID 为 `ua-pro`。无 npm 运行依赖。
6
+
7
+ ## 安装与打开管理页
8
+
9
+ 在 Magpie「插件 → 添加插件」填写 npm 来源:
10
+
11
+ ```text
12
+ opencode-claude-code-ua@0.3.6
13
+ ```
14
+
15
+ 也可以使用终端:
16
+
17
+ ```sh
18
+ magpie plugin add "opencode-claude-code-ua@0.3.6"
19
+ magpie plugin login ua-pro
20
+ ```
21
+
22
+ 登录方式中选择 **打开 UA PRO 管理**。已经登录账号后,也可再次执行登录命令打开管理页,管理其他连接和模板。插件内嵌日芒 SVG 图标,无需联网加载图片;界面支持纯白、纯黑及跟随系统主题。
23
+
24
+ 如需使用 GitHub 来源:
25
+
26
+ ```sh
27
+ magpie plugin add "github:1466109846/ua-pro"
28
+ ```
29
+
30
+ 同一插件请选择一种安装来源,不要重复安装。安装或更新不会自动迁移其他插件的配置。
31
+
32
+ ### 独立运行管理页
33
+
34
+ 需要 Node.js 22 或以上。下载仓库或 Release 后,在插件目录运行:
35
+
36
+ ```sh
37
+ node setup.mjs
38
+ ```
39
+
40
+ 打开终端输出的一次性回环链接。服务仅监听本机,不默认监听公网。独立工具保存连接后,需要在 Magpie 重载插件;宿主与独立工具应指向同一个 UA 状态文件。
41
+
42
+ ## 快速配置
43
+
44
+ 1. 打开 **连接 → 新增连接**,填写名称、上游 Base URL、认证方式和 API key。
45
+ 2. 点击 **获取模型**,按当前表单中的地址和凭据查询;新连接无需先保存。
46
+ 3. 多选模型,选择 **追加选中模型**或**替换为选中模型**,然后保存连接。
47
+ 4. 选择自动模板,或指定一个已保存的模板版本。
48
+ 5. 在测试页检查最终地址、模型、协议和模板,再确认发送请求。
49
+ 6. 在 Magpie 重新获取模型或重载插件,让宿主载入保存的连接和路由。
50
+
51
+ 客户端连接的是 **Magpie 本地网关**,使用 Magpie 网关密钥,而不是直接填写上游 key。模型引用请从测试页复制:
52
+
53
+ - 主池:`ua-pro/<模型 ID>`。
54
+ - 独立站点:`ua-pro-<siteId>/<模型 ID>`。
55
+
56
+ 独立站点的模型不能写成主池引用。客户端协议转换由 Magpie 负责;UA PRO 接收转换后的请求,执行对应出站模板并使用所选账号凭据。
57
+
58
+ ## 连接与模型
59
+
60
+ 每个连接支持启停、自动或固定上游协议、模型列表、模型级协议覆盖、协议根路径,以及连接级和模型级模板绑定。
61
+
62
+ Base URL 可填写站点根地址,或以 `/v1`、`/v1beta` 结尾的地址。插件去除末尾版本段,再按协议拼接路径;自定义根路径可以保留部署前缀,例如 `anthropic: "/anthropic/v1"`。不能填写外域 URL、查询参数或路径穿越。远程地址要求 HTTPS,本机回环可使用 HTTP。
63
+
64
+ | 认证方式 | 发送方式 |
65
+ |---|---|
66
+ | `api-key` | Messages 使用 `x-api-key`,Gemini 使用 `x-goog-api-key`,Responses / Chat 使用 Bearer |
67
+ | `bearer` | 始终使用 Bearer,填写纯 key |
68
+ | `custom` | 填写完整 Authorization 值 |
69
+ | `none` | 不发送认证 |
70
+
71
+ 编辑已保存连接时,同一地址留空 key 可以复用原密钥;更换地址需要重新填写凭据。原生 Magpie 账号的 key 由宿主保存,UA PRO 仅记录账号引用,并核对账号端点;同一账号槽换站后,旧连接不能使用新站点凭据。
72
+
73
+ 模型查询只读取列表,不发送生成请求、不改变模板绑定,也不会清空手填模型。追加/替换只修改表单草稿,保存后才持久化。失败、取消和空列表都会保留手填内容;目标或凭据变更会使旧查询结果失效。
74
+
75
+ 宿主与管理页共用按目标和凭据隔离的模型发现目录,显示来源和时间。点击 **获取模型**始终尝试实时查询;网络失败时可显示过去 24 小时的同作用域目录,并标明缓存及失败原因。401、403、无效响应或主动取消不会用缓存冒充成功,成功的空列表会替换旧目录。
76
+
77
+ 默认多个账号共用 `ua-pro` 主池。勾选独立显示并填写 `siteId` 后,供应商为 `ua-pro-<siteId>`;最多 8 个独立站点槽,每个站点可有多个账号。站点增删需要重载宿主。
78
+
79
+ ### 生产路由与测试路由
80
+
81
+ 模型级协议和模板绑定优先于连接默认配置。自动协议按模型名推断,不代表上游已经支持该接口;明确知道端点类型时,应保存固定协议或模型覆盖。
82
+
83
+ 测试页会显示 **已保存的生产上游**。仅切换测试协议不会修改生产配置。如需应用,可点击 **将本次协议与模板保存为此模型路由**,一次性保存该模型的协议和精确模板版本,其他模型保持不变;随后在 Magpie 重新获取模型或重载插件。
84
+
85
+ 插件不在失败后自动更换 key、模型或协议。Magpie 自身的路由与 fallback 需单独配置。
86
+
87
+ ## 模板与版本
88
+
89
+ 首次加载建立三份初始模板,已有记录不会因启动而被覆盖:
90
+
91
+ | 模板 | 类型 | 默认行为 |
92
+ |---|---|---|
93
+ | Codex | 内置只读 | Responses 请求头、元数据及会话兼容;固定基线版本 0.159.0 |
94
+ | Claude Code | 内置只读 | Messages 请求头、条件性 system / user_id 补全及 fast beta 联动;固定 CLI 基线 2.1.296 |
95
+ | Claude Code VS Code 2.1.316 | 可编辑独立模板 | 固定 UA 和 14 项请求头,默认不补客户端身份正文 |
96
+
97
+ 内置基线不自动联网追踪新版本。自动绑定按最终上游协议选择:Responses → Codex,Messages → Claude Code,Chat / Gemini → 普通转发;绑定仍锁定具体模板版本。
98
+
99
+ ### 新增、保存与应用
100
+
101
+ 1. 在 **模板库 → 新增模板**选择内置或已有模板版本作为基线。
102
+ 2. 完整复制为独立草稿,修改名称、UA、请求头、会话或正文规则。
103
+ 3. **保存新版本**,先预览或测试,再 **应用**到连接或模型。
104
+
105
+ 自定义模板是完整副本,不在运行时继承原模板。保存 v2 不影响仍绑定 v1 的连接。版本历史支持比较、查看和回退;被引用的版本不能删除。归档不会使已有绑定失效,但不能新应用归档模板。
106
+
107
+ JSON 导入只建立经校验的草稿,不自动保存或应用;导出为脱敏规则副本。自定义普通文本和附加请求头分享前仍需人工检查。
108
+
109
+ ### 请求头与参数规则
110
+
111
+ 请求头支持 `set`、`set-if-missing`、`set-if-empty`、`delete`、`preserve`,可限定条件和操作路径。模板变量仅使用支持的上下文,例如 `{{session.id}}`、`{{model.id}}`、`{{request.id}}`、`{{client.version}}`;缺少变量时跳过覆盖,不发送未展开的字符串。
112
+
113
+ 认证最后写入。模板不能覆盖 Authorization、API key、Cookie、Host、Content-Length 等认证或传输控制头。模板使用声明式规则,不执行用户脚本。
114
+
115
+ 参数按 **`defaults` → `overrides` → `remove`**处理:补缺、显式覆盖、最后删除。默认保护模型、消息、工具和流式选择。`count_tokens` / Gemini `countTokens` 不执行生成参数覆盖或 Claude 身份正文补全。
116
+
117
+ ### 正文与会话兼容
118
+
119
+ 客户端原生判定仅基于转换前可见 UA,是启发式,不验证官方客户端。宿主可能将原 UA 替换为 `magpie/...`,插件不会假称能够恢复它。
120
+
121
+ 内置 Claude 模板对非原生 Messages 请求可补 system 身份块及兼容 `metadata.user_id`,并补缺失的 `x-claude-code-session-id`,保持会话一致。已有原生会话头不改写,`preserve` / `off` 策略不会凭空新增。Codex 模板可提升允许的 `client_metadata` 头,并生成按连接和会话隔离的兼容标识。
122
+
123
+ 这些显式兼容规则可能改变提示前缀、输入 token 或缓存;可复制模板后查看、修改或关闭。正文读取有体积、时限和取消保护,原生已有账单字段不伪造或替换。
124
+
125
+ ## 三层测试
126
+
127
+ | 测试 | 网络行为与结论 |
128
+ |---|---|
129
+ | 本地转换预览 | 不联网,展示同一转换函数准备的脱敏 URL、headers、body 和规则变化 |
130
+ | 模型列表检查 | 明确确认后读取目录,不证明模型能够生成回复 |
131
+ | 实际模型请求 | 明确确认后直测所选连接,支持普通/流式,可能产生费用,不自动回退 |
132
+
133
+ 测试页自动载入已保存模型及同凭据的发现目录,优先选择已保存模型。实际请求模式下没有已知模型时,会先读取一次目录,但 **不会自动发送生成请求**;离线预览不自动联网。也可展开高级选项手动填写真实模型 ID。
134
+
135
+ 默认 **自动适配本次协议**会保留兼容的连接绑定;绑定不兼容时,本次明确选用相应内置模板,生产绑定不变。选择严格绑定或指定版本时,不兼容会在发送前阻止请求。
136
+
137
+ 确认框显示最终 URL、模型、协议和模板。高级根路径覆盖只用于本次测试,不修改连接。自定义正文在切换协议或模型后保留,结构不兼容时需明确替换为样例。
138
+
139
+ 结果显示连接、模型、测试时间、HTTP 状态、耗时、首块时间、响应和上游 usage。表单变化后,旧结果标为过期;HTTP 200 中的错误 JSON 或错误 SSE 不算成功,取消和超时分别说明。测试不修改绑定;出站预览不代表站点确认接收了所有字段。
140
+
141
+ ## 保存、应用与回退
142
+
143
+ ```text
144
+ 编辑草稿 → 保存模板版本 → 预览/测试 → 应用到连接或模型
145
+ ```
146
+
147
+ 请求开始时冻结模板、连接和凭据快照,在途请求不随编辑改变。保存与应用分离;保存文件不等于正在运行的宿主已载入。独立工具无法观察另一进程的运行状态,会显示等待宿主确认。
148
+
149
+ 写操作使用 operationId 防止重复提交,网络断开后可查询原操作。失败保留草稿,结果未知时阻止盲目重提;多窗口有 revision / generation 冲突检查,跨进程不提供严格原子 CAS。
150
+
151
+ ## 兼容入口与 New API
152
+
153
+ 旧 `claude-code-ua` 供应商保留兼容,原模型引用仍可使用。UA PRO 是唯一连接/模板管理前端,旧书签跳转到统一页面。设置中可预览旧 profile 并明确复制为新连接,不自动删除旧配置或复制原生账号 key。
154
+
155
+ New API 账号管理、余额查询、手动签到和按 IANA 时区的每日签到保留。站点 PAT / Cookie 与模型 key 分开;定时器仅宿主运行时启用,独立工具不自动签到。余额绑定依赖宿主明确提供账号作用域,旧宿主不支持时不会猜测账号。
156
+
157
+ ## 数据与安全
158
+
159
+ | 默认文件 | 用途 |
160
+ |---|---|
161
+ | `runtime/ua-pro.json` | 模板、连接、绑定、generation 及本地保存的连接凭据 |
162
+ | `runtime/ua-pro.json.models.json` | 模型发现目录、来源、时间和作用域摘要,不含 key |
163
+ | `runtime/profile.json` | 旧 Claude 配置,兼容保留 |
164
+ | `runtime/accounts.json` | New API 站点账号 |
165
+ | `runtime/schedule-claims/` | 每日签到防重复记录 |
166
+
167
+ `uaFile` 插件选项或 `UA_PRO_FILE` 环境变量可指定 UA 状态文件绝对路径。`profileFile` / `MAGPIE_CLAUDE_PROFILE_FILE` 指定旧配置;独立工具的 `--config` 指定旧 profile 路径,默认 UA 文件放在同目录,也可由 `UA_PRO_FILE` 覆盖。
168
+
169
+ 本地凭据以文件权限保护的明文保存,不是加密保险库。POSIX 使用 0600 / 0700,Windows 依赖 NTFS ACL。不要分享或提交 runtime、配置副本或附加秘密请求头。npm 包仅包含运行代码、图标、README 和必要许可证,不含用户数据或开发测试。
170
+
171
+ 仅连接自己拥有或已获授权的 API。UA 和兼容标识不证明官方客户端身份,不提供 OAuth、设备认证、签名或额外账户权限,也不能保证任意上游接受请求。
172
+
173
+ ## 常见问题
174
+
175
+ **管理页如何再次打开?**
176
+
177
+ 执行 `magpie plugin login ua-pro`,选择 **打开 UA PRO 管理**。管理会话过期后需重新打开;刷新旧页面不能延长授权。
178
+
179
+ **宿主能获取模型,独立管理工具却失败?**
180
+
181
+ 从 Magpie 打开管理页后,查询可复用最近 15 分钟同目标/凭据模型发现的账号网络上下文,包括账号代理。独立 Node 进程不能继承 Magpie 账号代理,需要自行配置网络;支持的 Node 版本可显式使用环境代理。插件不硬编码本地代理端口,也没有独立代理设置面板。
182
+
183
+ **模型列表成功,但生成返回 404?**
184
+
185
+ 确认生产模型路由、实际协议根路径,以及独立站点的供应商前缀。目录成功不代表所有生成端点可用;测试页直测上游,客户端到上游的协议转换由 Magpie 根据已保存路由完成。
186
+
187
+ **修改后为什么没有生效?**
188
+
189
+ 模板先保存,再应用;连接、站点或模型协议更新后,在 Magpie 重新获取模型或重载插件,并重新打开管理页。不要将文件保存或单次测试成功当作生产生效证明。
190
+
191
+ ## 反馈与许可
192
+
193
+ - [GitHub 仓库](https://github.com/1466109846/ua-pro)
194
+ - [问题反馈](https://github.com/1466109846/ua-pro/issues)
195
+ - [npm 包](https://www.npmjs.com/package/opencode-claude-code-ua)
196
+
197
+ 本项目按 MIT 许可分发,版权和第三方来源见 [LICENSE](LICENSE) 与 [NOTICE.md](NOTICE.md)。
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Magpie Claude Code UA">
2
+ <rect width="64" height="64" rx="14" fill="#3b2b24"/>
3
+ <g fill="none" stroke="#f0b79b" stroke-width="3.2" stroke-linecap="round">
4
+ <path d="M32 11.5v9.2M32 43.3v9.2M11.5 32h9.2M43.3 32h9.2"/>
5
+ <path d="M17.4 17.4l6.5 6.5M40.1 40.1l6.5 6.5M46.6 17.4l-6.5 6.5M23.9 40.1l-6.5 6.5"/>
6
+ </g>
7
+ <circle cx="32" cy="32" r="7" fill="#e6a184"/>
8
+ <circle cx="32" cy="32" r="2.4" fill="#3b2b24"/>
9
+ </svg>
package/index.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { createPlugin } from './src/plugin.mjs'
2
+ import { pluginOptions } from './src/options.mjs'
3
+ import { createUAPlugin, createUASite } from './src/ua-plugin.mjs'
4
+
5
+ export default async function ClaudeCodeUAPlugin(input, options) {
6
+ if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) throw new TypeError('Plugin options must be an object')
7
+ const legacy = { ...options }
8
+ delete legacy.uaFile
9
+ return createPlugin(input, legacy)
10
+ }
11
+
12
+ export const UAProPlugin = async (input, options) => createUAPlugin(input, options)
13
+ export const UAProSite1 = async (input, options) => createUASite(input, options, 0)
14
+ export const UAProSite2 = async (input, options) => createUASite(input, options, 1)
15
+ export const UAProSite3 = async (input, options) => createUASite(input, options, 2)
16
+ export const UAProSite4 = async (input, options) => createUASite(input, options, 3)
17
+ export const UAProSite5 = async (input, options) => createUASite(input, options, 4)
18
+ export const UAProSite6 = async (input, options) => createUASite(input, options, 5)
19
+ export const UAProSite7 = async (input, options) => createUASite(input, options, 6)
20
+ export const UAProSite8 = async (input, options) => createUASite(input, options, 7)
21
+
22
+ // The host calls every exported function; helpers therefore live on an object.
23
+ export const _internal = Object.freeze({ createPlugin, pluginOptions, createUAPlugin, createUASite })
@@ -0,0 +1,108 @@
1
+ const protectedFields = new Set(["model", "messages", "stream", "tools", "tool_choice"])
2
+ const responsesFields = new Set([
3
+ "store", "include", "instructions", "input", "reasoning", "max_output_tokens",
4
+ "parallel_tool_calls", "prompt_cache_key", "response_format", "stream_options",
5
+ "previous_response_id", "conversation", "background", "truncation",
6
+ ])
7
+ const unsafeKeys = new Set(["__proto__", "constructor", "prototype"])
8
+
9
+ export function isObject(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value)
11
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
12
+ }
13
+
14
+ export function copyJSON(value, depth = 0) {
15
+ if (depth > 32) throw new TypeError("JSON depth exceeds 32 (possibly cyclic)")
16
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value
17
+ if (typeof value === "number" && Number.isFinite(value)) return value
18
+ if (Array.isArray(value)) return value.map((item) => copyJSON(item, depth + 1))
19
+ if (!isObject(value)) throw new TypeError("Settings must contain only JSON values")
20
+ const result = {}
21
+ for (const [key, item] of Object.entries(value)) {
22
+ if (unsafeKeys.has(key)) throw new TypeError(`Unsafe option key: ${key}`)
23
+ result[key] = copyJSON(item, depth + 1)
24
+ }
25
+ return result
26
+ }
27
+
28
+ export function compileParameters(options = {}) {
29
+ if (!isObject(options)) throw new TypeError("parameters must be an object")
30
+ for (const key of Object.keys(options)) {
31
+ if (!["defaults", "overrides", "remove"].includes(key)) throw new TypeError(`Unknown parameters option: ${key}`)
32
+ }
33
+ const plan = {}
34
+ for (const section of ["defaults", "overrides"]) {
35
+ const value = options[section] === undefined ? {} : options[section]
36
+ if (!isObject(value)) throw new TypeError(`parameters.${section} must be an object`)
37
+ for (const key of Object.keys(value)) {
38
+ if (protectedFields.has(key)) throw new TypeError(`parameters.${section}.${key} is protected`)
39
+ if (responsesFields.has(key)) throw new TypeError(`${key} is not an Anthropic Messages parameter`)
40
+ }
41
+ plan[section] = copyJSON(value)
42
+ }
43
+ const remove = options.remove === undefined ? [] : options.remove
44
+ if (!Array.isArray(remove)) throw new TypeError("parameters.remove must be an array of object paths")
45
+ plan.remove = remove.map((path) => {
46
+ if (typeof path !== "string" || !/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(path)) {
47
+ throw new TypeError("parameters.remove must contain nonempty dotted object paths (no array indexes)")
48
+ }
49
+ const keys = path.split(".")
50
+ if (keys.some((key) => unsafeKeys.has(key))) throw new TypeError(`Unsafe remove path: ${path}`)
51
+ if (protectedFields.has(keys[0])) throw new TypeError(`parameters.remove: ${keys[0]} is protected`)
52
+ return keys
53
+ })
54
+ plan.active = Object.keys(plan.defaults).length > 0 || Object.keys(plan.overrides).length > 0 || plan.remove.length > 0
55
+ return plan
56
+ }
57
+
58
+ function merge(target, source, defaults) {
59
+ for (const [key, value] of Object.entries(source)) {
60
+ if (!Object.hasOwn(target, key)) {
61
+ target[key] = copyJSON(value)
62
+ } else if (isObject(value) && isObject(target[key])) {
63
+ // A different thinking mode must not inherit the previous mode's budget.
64
+ if (key === "thinking" && Object.hasOwn(value, "type") && value.type !== target[key].type) {
65
+ if (!defaults) target[key] = copyJSON(value)
66
+ } else merge(target[key], value, defaults)
67
+ } else if (!defaults) target[key] = copyJSON(value)
68
+ }
69
+ }
70
+
71
+ function validate(body) {
72
+ if (!Number.isSafeInteger(body.max_tokens) || body.max_tokens < 0) throw new TypeError("max_tokens must be a nonnegative safe integer")
73
+ for (const key of ["temperature", "top_p"]) {
74
+ if (Object.hasOwn(body, key) && (typeof body[key] !== "number" || !Number.isFinite(body[key]) || body[key] < 0 || body[key] > 1)) {
75
+ throw new TypeError(`${key} must be a number between 0 and 1`)
76
+ }
77
+ }
78
+ if (Object.hasOwn(body, "top_k") && (!Number.isSafeInteger(body.top_k) || body.top_k < 0)) throw new TypeError("top_k must be a nonnegative safe integer")
79
+ if (!Object.hasOwn(body, "thinking")) return
80
+ const thinking = body.thinking
81
+ if (!isObject(thinking) || typeof thinking.type !== "string" || !thinking.type.trim()) throw new TypeError("thinking.type must be a nonempty string")
82
+ if (thinking.type === "enabled") {
83
+ // Interleaved thinking can legitimately budget more than max_tokens.
84
+ if (!Number.isSafeInteger(thinking.budget_tokens) || thinking.budget_tokens < 1024) {
85
+ throw new TypeError("thinking.budget_tokens must be an integer >= 1024")
86
+ }
87
+ } else if (["adaptive", "disabled", "between_tools"].includes(thinking.type) && Object.hasOwn(thinking, "budget_tokens")) {
88
+ throw new TypeError("budget_tokens is only valid with thinking.type enabled")
89
+ }
90
+ }
91
+
92
+ export function applyParameters(body, plan) {
93
+ if (!plan.active) return body
94
+ if (!isObject(body)) throw new TypeError("Messages body must be a JSON object")
95
+ const result = structuredClone(body)
96
+ merge(result, plan.defaults, true)
97
+ merge(result, plan.overrides, false)
98
+ for (const keys of plan.remove) {
99
+ let parent = result
100
+ for (const key of keys.slice(0, -1)) {
101
+ if (!isObject(parent) || !Object.hasOwn(parent, key)) { parent = undefined; break }
102
+ parent = parent[key]
103
+ }
104
+ if (isObject(parent)) delete parent[keys.at(-1)]
105
+ }
106
+ validate(result)
107
+ return result
108
+ }
@@ -0,0 +1,43 @@
1
+ // Shared URL/routing primitives. Keep runtime and browser test plans on one path contract.
2
+ export const SDK_BY_PROTOCOL = Object.freeze({ anthropic: '@ai-sdk/anthropic', responses: '@ai-sdk/openai', 'chat-completions': '@ai-sdk/openai-compatible', gemini: '@ai-sdk/google' })
3
+ export const PATHS = { anthropic: '/v1', responses: '/v1', 'chat-completions': '/v1', gemini: '/v1beta' }
4
+ const err = (message, status = 400) => Object.assign(new Error(message), { status })
5
+
6
+ export function protocolForModel(connection, model = '') {
7
+ const configured = connection.modelProtocols?.[model] ?? connection.protocol ?? 'auto'
8
+ if (configured !== 'auto') {
9
+ if (!Object.hasOwn(SDK_BY_PROTOCOL, configured)) throw err('不支持的上游协议')
10
+ return configured
11
+ }
12
+ const name = model.toLowerCase().split('/').at(-1)
13
+ if (/^claude[-.]/.test(name) || model.startsWith('anthropic/')) return 'anthropic'
14
+ if (/^(gpt-|o\d|codex-|chatgpt-)/.test(name)) return 'responses'
15
+ if (/^gemini[-.]/.test(name)) return 'gemini'
16
+ return 'chat-completions'
17
+ }
18
+
19
+ export function siteRoot(baseURL) {
20
+ let url
21
+ try { url = new URL(baseURL) } catch { throw err('请配置有效的上游地址') }
22
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw err('上游地址不能包含凭据、查询或片段')
23
+ if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) throw err('远程上游必须使用 HTTPS')
24
+ return url.href.replace(/\/+$/, '').replace(/\/(v1|v1beta)$/, '')
25
+ }
26
+
27
+ export function apiBase(connection, protocol) {
28
+ const path = connection.apiPaths?.[protocol] ?? PATHS[protocol]
29
+ if (typeof path !== 'string' || !/^\/(?!\/)[A-Za-z0-9/_-]*$/.test(path)) throw err('协议根路径无效')
30
+ return siteRoot(connection.baseURL) + path.replace(/\/+$/, '')
31
+ }
32
+
33
+ export function operationURL(connection, protocol, operation, model) {
34
+ let suffix = operation
35
+ if (protocol === 'anthropic' && operation === 'count_tokens') suffix = 'messages/count_tokens'
36
+ if (protocol === 'gemini') {
37
+ if (!['generateContent', 'streamGenerateContent', 'countTokens'].includes(operation)) throw err('Gemini 操作无效')
38
+ suffix = `models/${encodeURIComponent(model)}:${operation}`
39
+ }
40
+ const allowed = { anthropic: ['messages', 'count_tokens'], responses: ['responses'], 'chat-completions': ['chat/completions'], gemini: ['generateContent', 'streamGenerateContent', 'countTokens'] }
41
+ if (!allowed[protocol]?.includes(operation)) throw err('操作与上游协议不匹配')
42
+ return apiBase(connection, protocol) + '/' + suffix
43
+ }