devinfra-bridge 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +121 -11
- package/dist/agents/identity.d.ts +14 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/config/load.d.ts +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +68 -18
- package/dist/main.js +66 -17
- package/dist/proxy/forward-messages.d.ts +2 -2
- package/dist/util/logger.d.ts +4 -0
- package/docs/acceptance.md +12 -2
- package/docs/development.md +30 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,98 @@
|
|
|
4
4
|
|
|
5
5
|
## 开发与运行
|
|
6
6
|
|
|
7
|
+
在仓库根目录执行,先用 `npm ci` 安装开发依赖。要求 Node.js >=22;启动命令的 `--config` 必须指向实际存在的绝对路径。
|
|
8
|
+
|
|
7
9
|
```bash
|
|
10
|
+
cd ~/code/devinfra/devinfra_bridge
|
|
8
11
|
npm ci
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### 命令速查
|
|
15
|
+
|
|
16
|
+
| 功能 | 命令 | 行为与前置条件 |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| 源码启动 | `npm run dev -- serve --config /absolute/path/bridge.json` | 用 tsx 前台运行最新 TS 源码,无需 build;不会自动监听文件变化或重启 |
|
|
19
|
+
| 调试重启 | `./scripts/debug-daemon.sh [--config /absolute/path/bridge.json]` | 构建、校验配置后结束占用 18765 的进程,前台启动最新产物;首次自动生成默认配置;macOS/Linux,需要 lsof |
|
|
20
|
+
| 类型检查 | `npm run typecheck` | 只检查类型,不生成产物 |
|
|
21
|
+
| lint / 格式检查 | `npm run lint` | 检查 src、tests、scripts,不改文件 |
|
|
22
|
+
| 格式化 / 自动修复 | `npm run format` | 写入格式调整和 Biome 可自动修复的内容 |
|
|
23
|
+
| 全部测试 | `npm test` | 执行源码与集成测试,使用假 JWT 和本机 mock Gateway |
|
|
24
|
+
| 构建最新代码 | `npm run build` | 重新生成 dist 中的 JS bundle 和类型声明 |
|
|
25
|
+
| 构建产物启动 | `npm start -- serve --config /absolute/path/bridge.json` | 前台运行 dist/main.js;修改源码后先 build |
|
|
26
|
+
| 文本版本 | `npm start -- --version` | 输出已构建产物的版本,无需配置文件 |
|
|
27
|
+
| JSON 版本 | `npm start -- --version --json` | 输出 `{"version":"..."}`,无需配置文件 |
|
|
28
|
+
| 打包运行验证 | `npm run smoke:artifact` | 先 build;npm pack 后临时解包,验证版本、启动、query 和 SIGTERM,不上传 npm |
|
|
29
|
+
| 完整发布前检查 | `npm run prepublishOnly` | 顺序执行 typecheck → lint → test → build → smoke:artifact,不上传 npm |
|
|
30
|
+
| 发布预演 | `npm run publish:dry` | 完整检查后列出包清单;允许当前功能分支,不上传、不要求 npm 登录 |
|
|
31
|
+
|
|
32
|
+
测试与打包运行验证会监听本机端口;生命周期测试和 smoke 使用 18765,执行前需要停止该端口的调试 daemon。普通启动命令遇到端口占用会失败,不会终止占用进程。
|
|
33
|
+
|
|
34
|
+
### 调试重启脚本
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 复用 ~/.local/state/devinfra-bridge/bridge.json;不存在时自动生成
|
|
38
|
+
./scripts/debug-daemon.sh
|
|
39
|
+
|
|
40
|
+
# 指定实际存在的绝对路径;支持含空格的路径
|
|
41
|
+
./scripts/debug-daemon.sh --config "/absolute/path/bridge.json"
|
|
42
|
+
|
|
43
|
+
# 首次生成配置时覆盖内置 Gateway(已有配置不会被覆盖)
|
|
44
|
+
DEVINFRA_GATEWAY_ORIGIN=https://another-gateway.example ./scripts/debug-daemon.sh
|
|
45
|
+
|
|
46
|
+
# 查看用法
|
|
47
|
+
./scripts/debug-daemon.sh --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
设置 `DEVINFRA_BRIDGE_STATE_ROOT` 时,默认配置路径改为该目录下的 `bridge.json`。首次运行且默认文件不存在时,脚本自动生成配置(权限 0600),使用内置 Gateway 和 PATH 中的 bytedcli;不写入 JWT。已有配置保持不变;显式 `--config` 指向不存在的文件时仍报错。脚本不修改 CC 文件、不安装 Node 或依赖,首次使用先执行 `npm ci`。
|
|
51
|
+
|
|
52
|
+
执行顺序:构建最新代码 → 必要时生成默认配置 → 校验运行时和配置 → 打印 TCP 18765 监听进程的 PID/进程名 → SIGTERM → 等待最多 12 秒 → 对仍监听的原进程 SIGKILL → 前台启动 `dist/main.js`。即使监听者不是 bridge,也会按该规则停止;权限不足或端口被其他新进程抢占时退出,不使用 sudo。构建或配置校验失败时保留当前进程。
|
|
53
|
+
|
|
54
|
+
终端显示就绪/启动错误;请求日志写入配置 `state_root` 下的 `daemon.log`,省略 state_root 时为 `~/.local/state/devinfra-bridge/daemon.log`。Ctrl+C 停止 daemon。此脚本供本机开发调试,不包含在 npm 发布包中。
|
|
55
|
+
|
|
56
|
+
### 请求日志与 Gateway LogID
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
tail -f ~/.local/state/devinfra-bridge/daemon.log
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
每条请求在完成或中断后写入日志(SSE 在流结束后);字段包括:
|
|
63
|
+
|
|
64
|
+
| 字段 | 含义 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `time` | 日志时间,UTC |
|
|
67
|
+
| `request_id` | daemon 生成的本机 UUID,不是云端 LogID |
|
|
68
|
+
| `gateway_logid` | Gateway 响应头 `x-tt-logid`,用于云端查日志 |
|
|
69
|
+
| `gateway_request_id` | Gateway 响应头 `request-id` |
|
|
70
|
+
| `gateway_x_request_id` | Gateway 响应头 `x-request-id` |
|
|
71
|
+
| `path` / `status` / `duration_ms` / `code` | 路由、返回状态、耗时、本机错误码 |
|
|
72
|
+
|
|
73
|
+
三个 Gateway ID 分别保存,不互相替代;云端未返回或值不符合长度/字符校验时省略。`x-tt-logid`、`request-id`、`x-request-id` 透传给客户端,仍遵守 HTTP connection header 的剥离规则。日志不记录 query 内容、JWT 或整组 headers,因此不能用 query 文本搜索历史请求。
|
|
74
|
+
|
|
75
|
+
### 常用执行组合
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 直接运行最新源码
|
|
79
|
+
npm run dev -- serve --config /absolute/path/bridge.json
|
|
80
|
+
|
|
81
|
+
# 构建后运行,行为与发布包一致
|
|
12
82
|
npm run build
|
|
83
|
+
npm start -- serve --config /absolute/path/bridge.json
|
|
84
|
+
|
|
85
|
+
# 也可直接调用服务入口
|
|
13
86
|
node dist/main.js --version --json
|
|
14
87
|
node dist/main.js serve --config /absolute/path/bridge.json
|
|
88
|
+
|
|
89
|
+
# 一次完成全部本地检查、构建和打包运行验证
|
|
90
|
+
npm run prepublishOnly
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
脚本本身也可直接执行(仍在仓库根目录):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node scripts/test.mjs # 等价于 npm test
|
|
97
|
+
node scripts/build.mjs # 等价于 npm run build
|
|
98
|
+
node scripts/smoke-artifact.mjs # 等价于 npm run smoke:artifact,先 build
|
|
15
99
|
```
|
|
16
100
|
|
|
17
101
|
`bridge.json`:
|
|
@@ -19,7 +103,7 @@ node dist/main.js serve --config /absolute/path/bridge.json
|
|
|
19
103
|
```json
|
|
20
104
|
{
|
|
21
105
|
"config_version": 1,
|
|
22
|
-
"gateway_origin": "https://
|
|
106
|
+
"gateway_origin": "https://devinfra-llm-gateway.bytedance.net",
|
|
23
107
|
"auth": {
|
|
24
108
|
"mode": "bytedcli",
|
|
25
109
|
"executable": "/absolute/path/bytedcli"
|
|
@@ -27,6 +111,8 @@ node dist/main.js serve --config /absolute/path/bridge.json
|
|
|
27
111
|
}
|
|
28
112
|
```
|
|
29
113
|
|
|
114
|
+
`gateway_origin` 可选,省略时使用内置 `https://devinfra-llm-gateway.bytedance.net`;显式配置优先。Messages 发往该 origin 下的 `/v1/messages`。`DEVINFRA_GATEWAY_ORIGIN` 只供调试脚本首次生成配置使用,daemon 本身不读取该环境变量。
|
|
115
|
+
|
|
30
116
|
`state_root` 可选,默认 `~/.local/state/devinfra-bridge`。使用同一 OS 用户及其认证环境启动 daemon;bytedcli 子进程继承 `BYTEDCLI_CLOUD_SITE` / `BYTEDCLI_PROFILE`,无 site 覆盖,固定 `--as user`,不执行登录、不读取 refresh token。
|
|
31
117
|
|
|
32
118
|
正常启动后 stdout 输出一行就绪 JSON。固定监听 `127.0.0.1:18765`;端口占用则退出。用 SIGINT/SIGTERM 停止,最多 10 秒排空 HTTP 请求,随后清理资源。
|
|
@@ -34,28 +120,52 @@ node dist/main.js serve --config /absolute/path/bridge.json
|
|
|
34
120
|
## 首版行为
|
|
35
121
|
|
|
36
122
|
- 只转发 `POST /v1/messages`(可带 `?beta=true`);原始 body、model、effort、thinking、工具字段、JSON/SSE 字节不转换。
|
|
37
|
-
-
|
|
123
|
+
- 注入 `x-use-agent-plan: 1`、`x-identity-token: <ByteCloud JWT>`、`x-identity-token-type: jwt`、`x-llm-app: devinfra-bridge:<agent>`;剥离客户端 Authorization、x-api-key、Cookie 等凭据。原有 `X-Devinfra-Agent: claude-code` 保留为本版路由标签。
|
|
38
124
|
- `devinfra-bridge:cc` 是 CC 配置中的固定占位 token,本版不校验;来源 header 表示服务路由,不能证明发送进程身份。
|
|
39
125
|
- 背压、取消、分阶段超时;401 等上游错误原样返回,模型请求不自动重试。
|
|
40
126
|
- `/v1/models`、`count_tokens`、管理接口返回 404;CLI、安装、模型配置、配置恢复、目录查询由其他项目负责。
|
|
41
127
|
|
|
42
128
|
更多见 [开发说明](docs/development.md)、[验收说明](docs/acceptance.md)和 [实施计划](docs/superpowers/plans/2026-11-09-cc-gateway-integration.md)。
|
|
43
129
|
|
|
130
|
+
### 请求来源标记
|
|
131
|
+
|
|
132
|
+
CLI 配置 agent 的占位凭据使用 `devinfra-bridge:{agent}`:CC 为 `devinfra-bridge:cc`,Codex 为 `devinfra-bridge:codex`,Hermes 为 `devinfra-bridge:hermes`,未知类型为 `devinfra-bridge:x`。CC 写在 settings.json 的 `env.ANTHROPIC_AUTH_TOKEN`;真实 JWT 只由 daemon 获取,不写入 agent 配置。
|
|
133
|
+
|
|
134
|
+
daemon 优先读取 Authorization Bearer 中的标记,再读取 x-api-key;没有显式标记时,通过已知 User-Agent 和 Codex originator 兜底识别。未知标记或无法识别的客户端统一为 x;不按 Messages/Responses 协议推断 agent。新增类型需同步 `src/agents/identity.ts` 注册表。所有上游身份 header 由共用转发函数覆盖生成,这套规则供后续协议接入复用,当前仍只支持 Messages 路由。
|
|
135
|
+
|
|
136
|
+
占位标记和客户端 header 都只用于来源统计,不认证发送进程;缺少或使用任意占位凭据不会被本机 daemon 拒绝。CLI 无需自行配置 Gateway 身份 header。
|
|
137
|
+
|
|
44
138
|
## npm 包与发布
|
|
45
139
|
|
|
46
140
|
包名 `devinfra-bridge`,首版 `0.1.0-alpha.1`,MIT 许可证。发布到公网 npm 的 `alpha` 标签,供外部 CLI 联调;真实 CC + Gateway 验收见验收说明。
|
|
47
141
|
|
|
142
|
+
所有发布脚本命令都会先执行完整检查;真实发布需 npm 公网登录凭据具有目标包写权限,并满足 2FA 要求。第一次发新包可通过账号登录并完成 2FA,或使用覆盖新包、启用 bypass 2FA 的 granular token;`npm whoami` 成功仅确认登录身份。
|
|
143
|
+
|
|
144
|
+
| 执行方式 | 行为 |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `./scripts/publish.sh --help` | 显示参数说明 |
|
|
147
|
+
| `./scripts/publish.sh` | 从 main/master/develop 发布,检查通过后询问 y/N |
|
|
148
|
+
| `./scripts/publish.sh --force` | 从当前功能分支发布,只跳过分支限制,仍检查并确认 |
|
|
149
|
+
| `./scripts/publish.sh --dry-run` | 检查并列出包清单,不上传;保留分支限制 |
|
|
150
|
+
| `./scripts/publish.sh --dry-run --force` | 功能分支发布预演,等价于 `npm run publish:dry` |
|
|
151
|
+
| `./scripts/publish.sh --yes` | 跳过最终 y/N,仍检查分支及完整验证;可用于 CI |
|
|
152
|
+
| `./scripts/publish.sh --force --yes` | 跳过分支限制和最终确认,检查通过后直接上传 |
|
|
153
|
+
|
|
48
154
|
```bash
|
|
49
|
-
#
|
|
50
|
-
npm
|
|
155
|
+
# 检查公网登录身份;缺少登录时按提示完成浏览器登录
|
|
156
|
+
npm whoami --registry=https://registry.npmjs.org/
|
|
157
|
+
npm login --auth-type=web --registry=https://registry.npmjs.org/
|
|
51
158
|
|
|
52
|
-
#
|
|
53
|
-
|
|
159
|
+
# 本机预演
|
|
160
|
+
npm run publish:dry
|
|
54
161
|
|
|
55
|
-
#
|
|
162
|
+
# 当前功能分支发布;脚本最终会询问确认
|
|
56
163
|
./scripts/publish.sh --force
|
|
164
|
+
|
|
165
|
+
# 发布成功后核对 alpha 标签
|
|
166
|
+
npm view devinfra-bridge@alpha version --registry=https://registry.npmjs.org/
|
|
57
167
|
```
|
|
58
168
|
|
|
59
|
-
|
|
169
|
+
发版只需手工修改 `package.json` 的 `version`;发布脚本以它为准,不要求 lockfile 根版本同步,仍校验 npm 包和构建产物版本。脚本不会自动修改版本、暂存或提交。预发布版本自动使用 alpha/beta/rc/next 标签,正式版本使用 latest;检查包括 typecheck、lint、测试、构建与解包运行。`npm publish` 本身也通过 `prepublishOnly` 执行这些检查。
|
|
60
170
|
|
|
61
171
|
外部 CLI 可添加依赖 `devinfra-bridge@alpha`,通过 Node.js >=22 启动安装包中的 `dist/main.js serve --config ...`,或导入 `startDaemon`。本包没有 `bin` 命令,用户 CLI 在独立项目中提供。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
declare const AGENT_IDENTITIES: readonly [{
|
|
3
|
+
readonly id: "cc";
|
|
4
|
+
readonly client: RegExp;
|
|
5
|
+
}, {
|
|
6
|
+
readonly id: "codex";
|
|
7
|
+
readonly client: RegExp;
|
|
8
|
+
}, {
|
|
9
|
+
readonly id: "hermes";
|
|
10
|
+
readonly client: RegExp;
|
|
11
|
+
}];
|
|
12
|
+
export type AgentIdentity = (typeof AGENT_IDENTITIES)[number]["id"] | "x";
|
|
13
|
+
export declare function identifyAgent(headers: IncomingHttpHeaders): AgentIdentity;
|
|
14
|
+
export {};
|
package/dist/agents/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { ProxyContext } from "../proxy/server.js";
|
|
3
3
|
export interface AgentRoute {
|
|
4
4
|
agent: string;
|
|
5
5
|
path: string;
|
|
6
6
|
method: string;
|
|
7
|
-
forward(req: IncomingMessage, res: ServerResponse, context: ProxyContext, signal: AbortSignal, agent: string): Promise<void>;
|
|
7
|
+
forward(req: IncomingMessage, res: ServerResponse, context: ProxyContext, signal: AbortSignal, agent: string, onUpstreamHeaders?: (headers: IncomingHttpHeaders) => void): Promise<void>;
|
|
8
8
|
}
|
package/dist/config/load.d.ts
CHANGED
package/dist/config/types.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { assertSupportedNode, loadDaemonConfig, parseDaemonConfig } from "./config/load.js";
|
|
2
|
-
export { CC_PLACEHOLDER_TOKEN, HOST, PORT } from "./config/types.js";
|
|
2
|
+
export { CC_PLACEHOLDER_TOKEN, DEFAULT_GATEWAY_ORIGIN, HOST, PORT } from "./config/types.js";
|
|
3
3
|
export type { AuthConfig, DaemonConfig } from "./config/types.js";
|
|
4
4
|
export { startDaemon } from "./daemon/daemon.js";
|
|
5
5
|
export type { DaemonHandle } from "./daemon/daemon.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,21 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { access, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { extname, isAbsolute, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/config/types.ts
|
|
8
|
+
var DEFAULT_GATEWAY_ORIGIN = "https://devinfra-llm-gateway.bytedance.net";
|
|
9
|
+
var HOST = "127.0.0.1";
|
|
10
|
+
var PORT = 18765;
|
|
11
|
+
var CC_PLACEHOLDER_TOKEN = "devinfra-bridge:cc";
|
|
12
|
+
var DEFAULT_LIMITS = Object.freeze({
|
|
13
|
+
bodyBytes: 32 * 1024 * 1024,
|
|
14
|
+
bodyIdleMs: 6e4,
|
|
15
|
+
concurrency: 8,
|
|
16
|
+
headersMs: 12e4,
|
|
17
|
+
streamIdleMs: 12e4
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/config/load.ts
|
|
6
21
|
var ConfigError = class extends Error {
|
|
7
22
|
code = "INVALID_CONFIG";
|
|
8
23
|
};
|
|
@@ -27,14 +42,15 @@ function parseDaemonConfig(value) {
|
|
|
27
42
|
const config = object(value);
|
|
28
43
|
onlyKeys(config, ["config_version", "gateway_origin", "state_root", "auth"]);
|
|
29
44
|
if (config.config_version !== 1) fail("Unsupported config_version; expected 1.");
|
|
30
|
-
|
|
45
|
+
const gatewayOrigin = config.gateway_origin === void 0 ? DEFAULT_GATEWAY_ORIGIN : config.gateway_origin;
|
|
46
|
+
if (typeof gatewayOrigin !== "string") fail("gateway_origin must be an HTTPS origin.");
|
|
31
47
|
let origin;
|
|
32
48
|
try {
|
|
33
|
-
origin = new URL(
|
|
49
|
+
origin = new URL(gatewayOrigin);
|
|
34
50
|
} catch {
|
|
35
51
|
return fail("gateway_origin must be an HTTPS origin.");
|
|
36
52
|
}
|
|
37
|
-
if (origin.protocol !== "https:" || origin.username || origin.password || origin.hash || origin.search || origin.pathname !== "/" || !/^https:\/\/[^/?#]+\/?$/.test(
|
|
53
|
+
if (origin.protocol !== "https:" || origin.username || origin.password || origin.hash || origin.search || origin.pathname !== "/" || !/^https:\/\/[^/?#]+\/?$/.test(gatewayOrigin))
|
|
38
54
|
fail("gateway_origin must be an HTTPS origin without credentials, path, query or fragment.");
|
|
39
55
|
const rawAuth = object(config.auth);
|
|
40
56
|
let auth;
|
|
@@ -93,18 +109,6 @@ function assertSupportedNode(version = process.versions.node) {
|
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
|
|
96
|
-
// src/config/types.ts
|
|
97
|
-
var HOST = "127.0.0.1";
|
|
98
|
-
var PORT = 18765;
|
|
99
|
-
var CC_PLACEHOLDER_TOKEN = "devinfra-bridge:cc";
|
|
100
|
-
var DEFAULT_LIMITS = Object.freeze({
|
|
101
|
-
bodyBytes: 32 * 1024 * 1024,
|
|
102
|
-
bodyIdleMs: 6e4,
|
|
103
|
-
concurrency: 8,
|
|
104
|
-
headersMs: 12e4,
|
|
105
|
-
streamIdleMs: 12e4
|
|
106
|
-
});
|
|
107
|
-
|
|
108
112
|
// src/daemon/daemon.ts
|
|
109
113
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
110
114
|
|
|
@@ -324,6 +328,33 @@ function sendError(res, error) {
|
|
|
324
328
|
res.end(body);
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
// src/agents/identity.ts
|
|
332
|
+
var AGENT_IDENTITIES = [
|
|
333
|
+
{ id: "cc", client: /^claude-cli(?:\/|\s|$)/i },
|
|
334
|
+
{
|
|
335
|
+
id: "codex",
|
|
336
|
+
client: /^(?:codex(?:_cli_rs|_exec|_vscode| desktop)?|acp-extension-codex|vscode_agent_host)(?:\/|\s|$)/i
|
|
337
|
+
},
|
|
338
|
+
{ id: "hermes", client: /^hermes-agent(?:\/|\s|$)/i }
|
|
339
|
+
];
|
|
340
|
+
function single(value) {
|
|
341
|
+
return typeof value === "string" ? value.trim() : "";
|
|
342
|
+
}
|
|
343
|
+
function identifyAgent(headers) {
|
|
344
|
+
const authorization = single(headers.authorization);
|
|
345
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(authorization)?.[1] ?? "";
|
|
346
|
+
for (const token of [bearer, single(headers["x-api-key"])]) {
|
|
347
|
+
if (!token.startsWith("devinfra-bridge:")) continue;
|
|
348
|
+
const id = token.slice("devinfra-bridge:".length);
|
|
349
|
+
return AGENT_IDENTITIES.find((agent) => agent.id === id)?.id ?? "x";
|
|
350
|
+
}
|
|
351
|
+
for (const client of [single(headers["user-agent"]), single(headers.originator)]) {
|
|
352
|
+
const agent = AGENT_IDENTITIES.find((agent2) => agent2.client.test(client));
|
|
353
|
+
if (agent) return agent.id;
|
|
354
|
+
}
|
|
355
|
+
return "x";
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
// src/proxy/headers.ts
|
|
328
359
|
var HOP = /* @__PURE__ */ new Set([
|
|
329
360
|
"connection",
|
|
@@ -346,6 +377,7 @@ var REQUEST = /* @__PURE__ */ new Set([
|
|
|
346
377
|
"request-id"
|
|
347
378
|
]);
|
|
348
379
|
var RESPONSE = /* @__PURE__ */ new Set([
|
|
380
|
+
"x-tt-logid",
|
|
349
381
|
"content-type",
|
|
350
382
|
"content-encoding",
|
|
351
383
|
"content-length",
|
|
@@ -367,8 +399,12 @@ function requestHeaders(incoming, jwt, bytes, agent) {
|
|
|
367
399
|
}
|
|
368
400
|
result["content-length"] = bytes;
|
|
369
401
|
result["x-identity-token"] = jwt;
|
|
402
|
+
result["x-identity-token-type"] = "jwt";
|
|
370
403
|
result["x-use-agent-plan"] = "1";
|
|
404
|
+
result["x-llm-app"] = `devinfra-bridge:${identifyAgent(incoming)}`;
|
|
371
405
|
result["x-devinfra-agent"] = agent;
|
|
406
|
+
result["x-use-ppe"] = "1";
|
|
407
|
+
result["x-tt-env"] = "ppe_devinfra_bridge";
|
|
372
408
|
return result;
|
|
373
409
|
}
|
|
374
410
|
function responseHeaders(incoming) {
|
|
@@ -430,7 +466,7 @@ async function readBody(req, context, signal) {
|
|
|
430
466
|
if (signal.aborted) onAbort();
|
|
431
467
|
});
|
|
432
468
|
}
|
|
433
|
-
async function forwardMessages(req, res, context, signal, agent) {
|
|
469
|
+
async function forwardMessages(req, res, context, signal, agent, onUpstreamHeaders) {
|
|
434
470
|
const body = await readBody(req, context, signal);
|
|
435
471
|
try {
|
|
436
472
|
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
|
|
@@ -479,6 +515,7 @@ async function forwardMessages(req, res, context, signal, agent) {
|
|
|
479
515
|
});
|
|
480
516
|
upstreamResponse = response;
|
|
481
517
|
clearTimeout(headerTimer);
|
|
518
|
+
onUpstreamHeaders?.(response.headers);
|
|
482
519
|
if ((response.statusCode ?? 502) >= 300 && (response.statusCode ?? 502) < 400) {
|
|
483
520
|
response.destroy();
|
|
484
521
|
throw new ProxyError(502, "UPSTREAM_REDIRECT", "Gateway redirection is not supported.");
|
|
@@ -558,6 +595,9 @@ import {
|
|
|
558
595
|
import { join as join2 } from "node:path";
|
|
559
596
|
var silentLogger = { write() {
|
|
560
597
|
} };
|
|
598
|
+
function safeTraceId(value) {
|
|
599
|
+
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
|
|
600
|
+
}
|
|
561
601
|
function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
562
602
|
const path = join2(stateRoot, "daemon.log");
|
|
563
603
|
const safeFile = (file) => {
|
|
@@ -573,6 +613,9 @@ function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
|
573
613
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
574
614
|
event: record.event,
|
|
575
615
|
request_id: record.request_id,
|
|
616
|
+
gateway_logid: safeTraceId(record.gateway_logid),
|
|
617
|
+
gateway_request_id: safeTraceId(record.gateway_request_id),
|
|
618
|
+
gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
|
|
576
619
|
path: record.path,
|
|
577
620
|
duration_ms: record.duration_ms,
|
|
578
621
|
status: record.status,
|
|
@@ -656,6 +699,7 @@ function createProxyHandler(context) {
|
|
|
656
699
|
const started = Date.now();
|
|
657
700
|
const requestId = randomUUID();
|
|
658
701
|
let code;
|
|
702
|
+
let upstreamHeaders;
|
|
659
703
|
const onAbort = () => controller.abort();
|
|
660
704
|
const onClose = () => {
|
|
661
705
|
if (!res.writableFinished) onAbort();
|
|
@@ -663,7 +707,9 @@ function createProxyHandler(context) {
|
|
|
663
707
|
req.once("aborted", onAbort);
|
|
664
708
|
res.once("close", onClose);
|
|
665
709
|
try {
|
|
666
|
-
await route.forward(req, res, context, controller.signal, route.agent)
|
|
710
|
+
await route.forward(req, res, context, controller.signal, route.agent, (headers) => {
|
|
711
|
+
upstreamHeaders = headers;
|
|
712
|
+
});
|
|
667
713
|
} catch (error) {
|
|
668
714
|
const failure = error instanceof ProxyError ? error : new ProxyError(502, "PROXY_FAILURE", "Request forwarding failed.");
|
|
669
715
|
code = failure.code;
|
|
@@ -675,6 +721,9 @@ function createProxyHandler(context) {
|
|
|
675
721
|
context.logger.write({
|
|
676
722
|
event: "request",
|
|
677
723
|
request_id: requestId,
|
|
724
|
+
gateway_logid: safeTraceId(upstreamHeaders?.["x-tt-logid"]),
|
|
725
|
+
gateway_request_id: safeTraceId(upstreamHeaders?.["request-id"]),
|
|
726
|
+
gateway_x_request_id: safeTraceId(upstreamHeaders?.["x-request-id"]),
|
|
678
727
|
path: "/v1/messages",
|
|
679
728
|
duration_ms: Date.now() - started,
|
|
680
729
|
status: res.statusCode,
|
|
@@ -722,7 +771,7 @@ async function prepareStateRoot(path) {
|
|
|
722
771
|
}
|
|
723
772
|
|
|
724
773
|
// src/version.ts
|
|
725
|
-
var VERSION = true ? "0.1.0-alpha.
|
|
774
|
+
var VERSION = true ? "0.1.0-alpha.2" : "0.1.0-dev";
|
|
726
775
|
|
|
727
776
|
// src/daemon/single-instance.ts
|
|
728
777
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -826,6 +875,7 @@ async function startDaemon(config) {
|
|
|
826
875
|
}
|
|
827
876
|
export {
|
|
828
877
|
CC_PLACEHOLDER_TOKEN,
|
|
878
|
+
DEFAULT_GATEWAY_ORIGIN,
|
|
829
879
|
HOST,
|
|
830
880
|
PORT,
|
|
831
881
|
assertSupportedNode,
|
package/dist/main.js
CHANGED
|
@@ -3,6 +3,20 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { access, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { extname, isAbsolute, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/config/types.ts
|
|
8
|
+
var DEFAULT_GATEWAY_ORIGIN = "https://devinfra-llm-gateway.bytedance.net";
|
|
9
|
+
var HOST = "127.0.0.1";
|
|
10
|
+
var PORT = 18765;
|
|
11
|
+
var DEFAULT_LIMITS = Object.freeze({
|
|
12
|
+
bodyBytes: 32 * 1024 * 1024,
|
|
13
|
+
bodyIdleMs: 6e4,
|
|
14
|
+
concurrency: 8,
|
|
15
|
+
headersMs: 12e4,
|
|
16
|
+
streamIdleMs: 12e4
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// src/config/load.ts
|
|
6
20
|
var ConfigError = class extends Error {
|
|
7
21
|
code = "INVALID_CONFIG";
|
|
8
22
|
};
|
|
@@ -27,14 +41,15 @@ function parseDaemonConfig(value) {
|
|
|
27
41
|
const config = object(value);
|
|
28
42
|
onlyKeys(config, ["config_version", "gateway_origin", "state_root", "auth"]);
|
|
29
43
|
if (config.config_version !== 1) fail("Unsupported config_version; expected 1.");
|
|
30
|
-
|
|
44
|
+
const gatewayOrigin = config.gateway_origin === void 0 ? DEFAULT_GATEWAY_ORIGIN : config.gateway_origin;
|
|
45
|
+
if (typeof gatewayOrigin !== "string") fail("gateway_origin must be an HTTPS origin.");
|
|
31
46
|
let origin;
|
|
32
47
|
try {
|
|
33
|
-
origin = new URL(
|
|
48
|
+
origin = new URL(gatewayOrigin);
|
|
34
49
|
} catch {
|
|
35
50
|
return fail("gateway_origin must be an HTTPS origin.");
|
|
36
51
|
}
|
|
37
|
-
if (origin.protocol !== "https:" || origin.username || origin.password || origin.hash || origin.search || origin.pathname !== "/" || !/^https:\/\/[^/?#]+\/?$/.test(
|
|
52
|
+
if (origin.protocol !== "https:" || origin.username || origin.password || origin.hash || origin.search || origin.pathname !== "/" || !/^https:\/\/[^/?#]+\/?$/.test(gatewayOrigin))
|
|
38
53
|
fail("gateway_origin must be an HTTPS origin without credentials, path, query or fragment.");
|
|
39
54
|
const rawAuth = object(config.auth);
|
|
40
55
|
let auth;
|
|
@@ -277,17 +292,6 @@ function createCredentialProvider(config) {
|
|
|
277
292
|
return config.mode === "bytedcli" ? new BytedcliProvider(config.executable) : new DirectJwtProvider(config);
|
|
278
293
|
}
|
|
279
294
|
|
|
280
|
-
// src/config/types.ts
|
|
281
|
-
var HOST = "127.0.0.1";
|
|
282
|
-
var PORT = 18765;
|
|
283
|
-
var DEFAULT_LIMITS = Object.freeze({
|
|
284
|
-
bodyBytes: 32 * 1024 * 1024,
|
|
285
|
-
bodyIdleMs: 6e4,
|
|
286
|
-
concurrency: 8,
|
|
287
|
-
headersMs: 12e4,
|
|
288
|
-
streamIdleMs: 12e4
|
|
289
|
-
});
|
|
290
|
-
|
|
291
295
|
// src/proxy/server.ts
|
|
292
296
|
import { randomUUID } from "node:crypto";
|
|
293
297
|
import { createServer } from "node:http";
|
|
@@ -323,6 +327,33 @@ function sendError(res, error) {
|
|
|
323
327
|
res.end(body);
|
|
324
328
|
}
|
|
325
329
|
|
|
330
|
+
// src/agents/identity.ts
|
|
331
|
+
var AGENT_IDENTITIES = [
|
|
332
|
+
{ id: "cc", client: /^claude-cli(?:\/|\s|$)/i },
|
|
333
|
+
{
|
|
334
|
+
id: "codex",
|
|
335
|
+
client: /^(?:codex(?:_cli_rs|_exec|_vscode| desktop)?|acp-extension-codex|vscode_agent_host)(?:\/|\s|$)/i
|
|
336
|
+
},
|
|
337
|
+
{ id: "hermes", client: /^hermes-agent(?:\/|\s|$)/i }
|
|
338
|
+
];
|
|
339
|
+
function single(value) {
|
|
340
|
+
return typeof value === "string" ? value.trim() : "";
|
|
341
|
+
}
|
|
342
|
+
function identifyAgent(headers) {
|
|
343
|
+
const authorization = single(headers.authorization);
|
|
344
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(authorization)?.[1] ?? "";
|
|
345
|
+
for (const token of [bearer, single(headers["x-api-key"])]) {
|
|
346
|
+
if (!token.startsWith("devinfra-bridge:")) continue;
|
|
347
|
+
const id = token.slice("devinfra-bridge:".length);
|
|
348
|
+
return AGENT_IDENTITIES.find((agent) => agent.id === id)?.id ?? "x";
|
|
349
|
+
}
|
|
350
|
+
for (const client of [single(headers["user-agent"]), single(headers.originator)]) {
|
|
351
|
+
const agent = AGENT_IDENTITIES.find((agent2) => agent2.client.test(client));
|
|
352
|
+
if (agent) return agent.id;
|
|
353
|
+
}
|
|
354
|
+
return "x";
|
|
355
|
+
}
|
|
356
|
+
|
|
326
357
|
// src/proxy/headers.ts
|
|
327
358
|
var HOP = /* @__PURE__ */ new Set([
|
|
328
359
|
"connection",
|
|
@@ -345,6 +376,7 @@ var REQUEST = /* @__PURE__ */ new Set([
|
|
|
345
376
|
"request-id"
|
|
346
377
|
]);
|
|
347
378
|
var RESPONSE = /* @__PURE__ */ new Set([
|
|
379
|
+
"x-tt-logid",
|
|
348
380
|
"content-type",
|
|
349
381
|
"content-encoding",
|
|
350
382
|
"content-length",
|
|
@@ -366,8 +398,12 @@ function requestHeaders(incoming, jwt, bytes, agent) {
|
|
|
366
398
|
}
|
|
367
399
|
result["content-length"] = bytes;
|
|
368
400
|
result["x-identity-token"] = jwt;
|
|
401
|
+
result["x-identity-token-type"] = "jwt";
|
|
369
402
|
result["x-use-agent-plan"] = "1";
|
|
403
|
+
result["x-llm-app"] = `devinfra-bridge:${identifyAgent(incoming)}`;
|
|
370
404
|
result["x-devinfra-agent"] = agent;
|
|
405
|
+
result["x-use-ppe"] = "1";
|
|
406
|
+
result["x-tt-env"] = "ppe_devinfra_bridge";
|
|
371
407
|
return result;
|
|
372
408
|
}
|
|
373
409
|
function responseHeaders(incoming) {
|
|
@@ -429,7 +465,7 @@ async function readBody(req, context, signal) {
|
|
|
429
465
|
if (signal.aborted) onAbort();
|
|
430
466
|
});
|
|
431
467
|
}
|
|
432
|
-
async function forwardMessages(req, res, context, signal, agent) {
|
|
468
|
+
async function forwardMessages(req, res, context, signal, agent, onUpstreamHeaders) {
|
|
433
469
|
const body = await readBody(req, context, signal);
|
|
434
470
|
try {
|
|
435
471
|
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
|
|
@@ -478,6 +514,7 @@ async function forwardMessages(req, res, context, signal, agent) {
|
|
|
478
514
|
});
|
|
479
515
|
upstreamResponse = response;
|
|
480
516
|
clearTimeout(headerTimer);
|
|
517
|
+
onUpstreamHeaders?.(response.headers);
|
|
481
518
|
if ((response.statusCode ?? 502) >= 300 && (response.statusCode ?? 502) < 400) {
|
|
482
519
|
response.destroy();
|
|
483
520
|
throw new ProxyError(502, "UPSTREAM_REDIRECT", "Gateway redirection is not supported.");
|
|
@@ -557,6 +594,9 @@ import {
|
|
|
557
594
|
import { join as join2 } from "node:path";
|
|
558
595
|
var silentLogger = { write() {
|
|
559
596
|
} };
|
|
597
|
+
function safeTraceId(value) {
|
|
598
|
+
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
|
|
599
|
+
}
|
|
560
600
|
function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
561
601
|
const path = join2(stateRoot, "daemon.log");
|
|
562
602
|
const safeFile = (file) => {
|
|
@@ -572,6 +612,9 @@ function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
|
572
612
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
573
613
|
event: record.event,
|
|
574
614
|
request_id: record.request_id,
|
|
615
|
+
gateway_logid: safeTraceId(record.gateway_logid),
|
|
616
|
+
gateway_request_id: safeTraceId(record.gateway_request_id),
|
|
617
|
+
gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
|
|
575
618
|
path: record.path,
|
|
576
619
|
duration_ms: record.duration_ms,
|
|
577
620
|
status: record.status,
|
|
@@ -655,6 +698,7 @@ function createProxyHandler(context) {
|
|
|
655
698
|
const started = Date.now();
|
|
656
699
|
const requestId = randomUUID();
|
|
657
700
|
let code;
|
|
701
|
+
let upstreamHeaders;
|
|
658
702
|
const onAbort = () => controller.abort();
|
|
659
703
|
const onClose = () => {
|
|
660
704
|
if (!res.writableFinished) onAbort();
|
|
@@ -662,7 +706,9 @@ function createProxyHandler(context) {
|
|
|
662
706
|
req.once("aborted", onAbort);
|
|
663
707
|
res.once("close", onClose);
|
|
664
708
|
try {
|
|
665
|
-
await route.forward(req, res, context, controller.signal, route.agent)
|
|
709
|
+
await route.forward(req, res, context, controller.signal, route.agent, (headers) => {
|
|
710
|
+
upstreamHeaders = headers;
|
|
711
|
+
});
|
|
666
712
|
} catch (error) {
|
|
667
713
|
const failure = error instanceof ProxyError ? error : new ProxyError(502, "PROXY_FAILURE", "Request forwarding failed.");
|
|
668
714
|
code = failure.code;
|
|
@@ -674,6 +720,9 @@ function createProxyHandler(context) {
|
|
|
674
720
|
context.logger.write({
|
|
675
721
|
event: "request",
|
|
676
722
|
request_id: requestId,
|
|
723
|
+
gateway_logid: safeTraceId(upstreamHeaders?.["x-tt-logid"]),
|
|
724
|
+
gateway_request_id: safeTraceId(upstreamHeaders?.["request-id"]),
|
|
725
|
+
gateway_x_request_id: safeTraceId(upstreamHeaders?.["x-request-id"]),
|
|
677
726
|
path: "/v1/messages",
|
|
678
727
|
duration_ms: Date.now() - started,
|
|
679
728
|
status: res.statusCode,
|
|
@@ -721,7 +770,7 @@ async function prepareStateRoot(path) {
|
|
|
721
770
|
}
|
|
722
771
|
|
|
723
772
|
// src/version.ts
|
|
724
|
-
var VERSION = true ? "0.1.0-alpha.
|
|
773
|
+
var VERSION = true ? "0.1.0-alpha.2" : "0.1.0-dev";
|
|
725
774
|
|
|
726
775
|
// src/daemon/single-instance.ts
|
|
727
776
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { ProxyContext } from "./server.js";
|
|
3
|
-
export declare function forwardMessages(req: IncomingMessage, res: ServerResponse, context: ProxyContext, signal: AbortSignal, agent: string): Promise<void>;
|
|
3
|
+
export declare function forwardMessages(req: IncomingMessage, res: ServerResponse, context: ProxyContext, signal: AbortSignal, agent: string, onUpstreamHeaders?: (headers: IncomingHttpHeaders) => void): Promise<void>;
|
package/dist/util/logger.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export interface LogRecord {
|
|
2
2
|
event: "request" | "ready" | "stopped" | "shutdown_error";
|
|
3
3
|
request_id?: string;
|
|
4
|
+
gateway_logid?: string;
|
|
5
|
+
gateway_request_id?: string;
|
|
6
|
+
gateway_x_request_id?: string;
|
|
4
7
|
path?: "/v1/messages";
|
|
5
8
|
duration_ms?: number;
|
|
6
9
|
status?: number;
|
|
@@ -10,4 +13,5 @@ export interface Logger {
|
|
|
10
13
|
write(record: LogRecord): void;
|
|
11
14
|
}
|
|
12
15
|
export declare const silentLogger: Logger;
|
|
16
|
+
export declare function safeTraceId(value: unknown): string | undefined;
|
|
13
17
|
export declare function createLogger(stateRoot: string, maxBytes?: number): Logger;
|
package/docs/acceptance.md
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
| 命令 | 结果 |
|
|
21
21
|
| --- | --- |
|
|
22
22
|
| `npm run typecheck` | 通过 |
|
|
23
|
-
| `npm run lint` | 通过,
|
|
23
|
+
| `npm run lint` | 通过,32 个源码/测试/脚本文件 |
|
|
24
24
|
| `npm run build` | 通过,JS bundle 和类型声明已生成 |
|
|
25
|
-
| `npm test` |
|
|
25
|
+
| `npm test` | 35 项通过,0 失败/跳过 |
|
|
26
26
|
| `npm run smoke:artifact` | 通过:打包、临时解包、版本、启动、query、SIGTERM;无需安装生产依赖 |
|
|
27
27
|
| `git diff --check` | 通过 |
|
|
28
28
|
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
新增 5 项回归测试,连同既有测试共 31 项全部通过。上述 typecheck、lint、build、smoke 和 diff 检查均已在修复后的实际仓库重新执行。
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
## 共用身份 header 与来源标记验收
|
|
41
|
+
|
|
42
|
+
2026-11-09 在 macOS / Node.js 22.22.3 重新执行 typecheck、lint、35 项测试、build 与 artifact smoke,全部通过;未发布 npm 包。
|
|
43
|
+
|
|
44
|
+
- 固定注入 `x-use-agent-plan: 1`、`x-identity-token-type: jwt`,JWT 来自 daemon 凭据源,`x-llm-app` 由来源识别生成,客户端同名 header 无法覆盖。
|
|
45
|
+
- 优先读取 Authorization Bearer / x-api-key 的 `devinfra-bridge:{agent}`;显式标记优先于原生客户端 header,未知标记统一 x。无标记时识别已知 User-Agent / Codex originator,通用 SDK 或未知客户端统一 x。
|
|
46
|
+
- 同一个 Messages 路由分别标记 cc/codex/hermes;未带占位 key 的请求仍正常工作并标记 x,不因协议被强制归为 cc。
|
|
47
|
+
- 普通请求、工具多轮、SSE 和解包产物均验证新增 header,原始 body 与响应保持不变;新增 4 项测试,总计 35 项。
|
|
48
|
+
- 现有 `X-Devinfra-Agent: claude-code` 保留为本版路由标签,来源统计使用新的 x-llm-app。来源标签不是本机鉴权;本期未增加 Responses/Codex 专属路由。
|
|
49
|
+
|
|
40
50
|
## 尚未进行的真实全链路验收
|
|
41
51
|
|
|
42
52
|
需要真实 Gateway HTTPS origin、员工 JWT-only Messages 能力、测试模型及外部准备好的 CC 配置。mock 通过不代表真实 CC + Gateway 已验收。
|
package/docs/development.md
CHANGED
|
@@ -23,7 +23,7 @@ flowchart LR
|
|
|
23
23
|
|
|
24
24
|
## 外部启动方
|
|
25
25
|
|
|
26
|
-
本工程交付 `dist/main.js` 与可导入的 `dist/index.js` / 类型声明,不提供用户 setup/disable 命令或 HTTP 管理 API。CLI/安装器准备 Node.js >=22、bytedcli、
|
|
26
|
+
本工程交付 `dist/main.js` 与可导入的 `dist/index.js` / 类型声明,不提供用户 setup/disable 命令或 HTTP 管理 API。CLI/安装器准备 Node.js >=22、bytedcli、CC 文件并管理进程。运行时仅拒绝低于 22 的 Node 版本;当前实际验收环境为 Node.js 22,构建目标保留 node22。
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
node /absolute/path/dist/main.js serve --config /absolute/path/bridge.json
|
|
@@ -32,16 +32,42 @@ node /absolute/path/dist/main.js serve --config /absolute/path/bridge.json
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
34
34
|
"config_version": 1,
|
|
35
|
-
"gateway_origin": "https://
|
|
35
|
+
"gateway_origin": "https://devinfra-llm-gateway.bytedance.net",
|
|
36
36
|
"state_root": "/absolute/path/user-state",
|
|
37
37
|
"auth": {"mode": "bytedcli", "executable": "/absolute/path/bytedcli"}
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
`gateway_origin` 可选,默认 `https://devinfra-llm-gateway.bytedance.net`;配置中显式值优先。Messages 请求路径为 `/v1/messages`,不在 origin 中添加协议路径。
|
|
42
|
+
|
|
41
43
|
bytedcli 参数固定为 `--no-auto-upgrade --json --as user auth get-bytecloud-jwt-token`。可执行入口支持绝对 native executable 路径,或 `.js/.mjs/.cjs`(由当前 Node 执行);不执行 `.cmd/.bat` 包装脚本,不启用 shell。Windows 的实际入口路径和三平台安装验收由后续迭代对齐。
|
|
42
44
|
|
|
43
45
|
bytedcli 默认获取/续取凭据。首版不自动在 Gateway 401 后强制刷新并重发模型请求。JWT 仅在内存存放,缓存最多 60 秒且不跨 `exp`(留 5 秒余量);不解析身份,不验签,Gateway 最终授权。
|
|
44
46
|
|
|
47
|
+
### 共用 Gateway header 与 agent 识别
|
|
48
|
+
|
|
49
|
+
`src/proxy/headers.ts` 为所有模型协议提供同一个请求 header 构造函数:
|
|
50
|
+
|
|
51
|
+
```http
|
|
52
|
+
x-use-agent-plan: 1
|
|
53
|
+
x-identity-token: <ByteCloud JWT>
|
|
54
|
+
x-identity-token-type: jwt
|
|
55
|
+
x-llm-app: devinfra-bridge:cc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
前两项固定注入策略不变,token-type 固定为 jwt,llm-app 的后缀来自 `src/agents/identity.ts`,均覆盖客户端同名字段。客户端 Authorization、x-api-key 和 Cookie 仍剥离。已有 `X-Devinfra-Agent: claude-code` 保留其本版路由标签语义;新的 x-llm-app 表示识别出的请求来源,不能据路由标签把所有 Messages 都算作 CC。
|
|
59
|
+
|
|
60
|
+
| 顺序 | 识别依据 | 结果 |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| 1 | Authorization Bearer 中的 `devinfra-bridge:{agent}` | 注册的 cc/codex/hermes;x 或未注册名称统一 x |
|
|
63
|
+
| 2 | x-api-key 中的同格式标记 | 同上;Authorization 中的显式标记优先 |
|
|
64
|
+
| 3 | 已知 User-Agent;再看 originator | claude-cli → cc;Codex CLI/exec/VS Code/Desktop/ACP/VS Code agent host 名称 → codex;hermes-agent → hermes |
|
|
65
|
+
| 4 | 缺少、通用 SDK/HTTP 库或无法识别的标识 | x |
|
|
66
|
+
|
|
67
|
+
显式 x 或未注册标记不再回退猜测;UA 按注册的客户端名称边界匹配,避免把相似产品名误识别。此判断不依赖请求 body、模型名或协议,也不查询本机进程。占位凭据仍不作鉴权,所有标识都是来源统计线索。
|
|
68
|
+
|
|
69
|
+
外部 CLI 将 CC 的 `env.ANTHROPIC_AUTH_TOKEN` 设为 `devinfra-bridge:cc`,后续 agent 使用相应标记。CLI 不写真实 JWT、不注入上述 Gateway header;新增 agent 同步注册表与配置实现。识别 codex/hermes 不表示已支持它们的其他协议,本期只有 Messages,后续 handler 必须复用该共用 header 函数。
|
|
70
|
+
|
|
45
71
|
若由外部提供已有 JWT,将 `auth` 替换为其一:
|
|
46
72
|
|
|
47
73
|
```json
|
|
@@ -58,7 +84,7 @@ bytedcli 默认获取/续取凭据。首版不自动在 Gateway 401 后强制刷
|
|
|
58
84
|
|
|
59
85
|
- 就绪输出:`{"event":"ready","version":"0.1.0-alpha.1","instance_id":"...","host":"127.0.0.1","port":18765}`。
|
|
60
86
|
- TCP 独占绑定是单实例锁;`instance.json` 只是诊断信息,不据此杀进程。崩溃遗留标记不阻止重启;仅绑定成功后替换标记。
|
|
61
|
-
- 日志 `daemon.log` + 两个轮转文件,每个约 5 MiB
|
|
87
|
+
- 日志 `daemon.log` + 两个轮转文件,每个约 5 MiB;包含内部 `request_id`、Gateway 响应中的 `gateway_logid`(x-tt-logid)、`gateway_request_id`(request-id)、`gateway_x_request_id`(x-request-id)、固定路由、耗时、状态和归一化错误码。请求完成/中断后记录;Gateway ID 缺失或不是 1–256 字符的字母数字及 `._:-` token 时省略。对应三个响应头透传给客户端;不记录 query/JWT/整组 headers。
|
|
62
88
|
- Body 最大 32 MiB、读取空闲 60 秒;并发最多 8;上游头等待和流空闲各 120 秒,无整体流总时限。
|
|
63
89
|
- 输入必须为未压缩 JSON;不重序列化。响应不解压,保留编码及有效长度。
|
|
64
90
|
- HTTPS 不跟随重定向;TLS 验证保持开启。企业 CA 用启动环境 `NODE_EXTRA_CA_CERTS`。首版不支持显式 HTTPS_PROXY 传输。
|
|
@@ -92,6 +118,6 @@ npm run publish:dry # 全量验证、列出包内容,不上传
|
|
|
92
118
|
|
|
93
119
|
`--yes` 可跳过最后的发布确认;`--dry-run --force` 可在功能分支验证。真实发布前检查 npm 公网登录态;如未登录,运行 `npm login --registry=https://registry.npmjs.org/`。npm 要求额外认证时按提示完成。
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
发版版本以 package.json 为准,只需手工修改其中的 version,不要求 package-lock.json 根版本同步;依赖变更时仍正常更新 lockfile。开发者自行决定何时提交,发布脚本不自动 bump 或提交。`prepublishOnly` 包含类型检查、lint、测试、build 与 artifact smoke,后者会核对打包版本和 bundle 版本,确保生产依赖为零时仍可运行。发布脚本显式执行该检查后再确认上传,因此最终 publish 使用 `--ignore-scripts` 避免重复执行。
|
|
96
122
|
|
|
97
123
|
alpha/beta/rc 版本对应同名 dist-tag,其他预发布版本使用 next,正式版本使用 latest。本期先发布 alpha 包给 CLI 项目联调,真实 CC + Gateway 未验收完成前不作为正式稳定版。
|