@wacry/codeks-bridge 0.0.6 → 0.0.7
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 +2 -2
- package/dist/backends/anthropic.js +1 -1
- package/dist/backends/chat.js +1 -1
- package/dist/backends/responses.js +1 -1
- package/dist/compaction-handoff.js +1 -0
- package/dist/config.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +15 -4
- package/dist/types.d.ts +14 -1
- package/dist/upstream.d.ts +1 -1
- package/dist/upstream.js +13 -7
- package/dist/writer.d.ts +2 -0
- package/dist/writer.js +14 -3
- package/docs/config.md +2 -1
- package/docs/embedding.md +37 -0
- package/docs/releasing.md +1 -1
- package/docs/responses-stream.md +1 -1
- package/docs/testing.md +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 安装和使用
|
|
8
8
|
|
|
9
|
-
需要 Node.js ≥
|
|
9
|
+
需要 Node.js ≥ 20.19 和已安装的 codeks。
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install -g @wacry/codeks-bridge
|
|
@@ -38,4 +38,4 @@ codeks-bridge config-snippet --config /absolute/path/bridge.json
|
|
|
38
38
|
|
|
39
39
|
`serve` 加 `--capture` 后打开日志里的 `/inspect/` 地址,可以逐条查看 codeks 请求、发往上游的请求、上游原始响应和写回 codeks 的响应,以及 reasoning 与压缩状态的回放决定。记录只在内存中,也可以在页面上开关捕获、清空或导出。详见[调试检查器](docs/inspect.md)。
|
|
40
40
|
|
|
41
|
-
[配置与模型目录](docs/config.md) · [整体设计](docs/design.md) · [Reasoning](docs/reasoning.md) · [压缩](docs/compaction.md) · [工具](docs/tools.md) · [调试检查器](docs/inspect.md) · [验证](docs/testing.md) · [发布](docs/releasing.md)
|
|
41
|
+
[配置与模型目录](docs/config.md) · [嵌入现有服务](docs/embedding.md) · [整体设计](docs/design.md) · [Reasoning](docs/reasoning.md) · [压缩](docs/compaction.md) · [工具](docs/tools.md) · [调试检查器](docs/inspect.md) · [验证](docs/testing.md) · [发布](docs/releasing.md)
|
|
@@ -253,7 +253,7 @@ function validateUsage(value, required) {
|
|
|
253
253
|
}
|
|
254
254
|
export const runAnthropic = async (context, writer) => {
|
|
255
255
|
const { body, tools, betas, continuationBefore } = buildAnthropicRequest(context);
|
|
256
|
-
const response = await upstreamFetch(context.route, "/messages", body, context.signal, context.requestHeaders, { "anthropic-beta": betas });
|
|
256
|
+
const response = await upstreamFetch(context.route, "/messages", body, context.signal, context.requestHeaders, { "anthropic-beta": betas }, context.fetch);
|
|
257
257
|
context.onAccepted?.(response.headers);
|
|
258
258
|
if (!response.body)
|
|
259
259
|
throw new BridgeError("Messages returned an empty stream.", "upstream_error", 502);
|
package/dist/backends/chat.js
CHANGED
|
@@ -195,7 +195,7 @@ async function* chatEvents(body, signal) {
|
|
|
195
195
|
export async function runChat(context, writer, dialect) {
|
|
196
196
|
const { body, tools } = buildChatRequest(context, dialect);
|
|
197
197
|
const { reasoningField } = dialect;
|
|
198
|
-
const response = await upstreamFetch(context.route, "chat/completions", body, context.signal, context.requestHeaders);
|
|
198
|
+
const response = await upstreamFetch(context.route, "chat/completions", body, context.signal, context.requestHeaders, undefined, context.fetch);
|
|
199
199
|
context.onAccepted?.(response.headers);
|
|
200
200
|
if (!response.body)
|
|
201
201
|
throw new BridgeError("Chat upstream returned no stream.", "upstream_error", 502);
|
|
@@ -92,7 +92,7 @@ function completedUsage(response) {
|
|
|
92
92
|
/** Transform only bridge-owned fields; native events keep their original shape. */
|
|
93
93
|
export const runResponses = async (context, writer) => {
|
|
94
94
|
writer.useNativeResponses();
|
|
95
|
-
const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined);
|
|
95
|
+
const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined, context.fetch);
|
|
96
96
|
context.onAccepted?.(response.headers);
|
|
97
97
|
const states = new Map();
|
|
98
98
|
const phases = new Map();
|
|
@@ -78,6 +78,7 @@ export class CompactionHandoffStore {
|
|
|
78
78
|
const result = await collectHandoff({
|
|
79
79
|
request: { ...request, model: route.slug }, route, signal,
|
|
80
80
|
requestHeaders: context.requestHeaders,
|
|
81
|
+
fetch: context.fetch,
|
|
81
82
|
responsesLite: carrier.handoff.responsesLite,
|
|
82
83
|
}, this.backends[route.profile.id]);
|
|
83
84
|
signal.throwIfAborted();
|
package/dist/config.js
CHANGED
|
@@ -60,8 +60,10 @@ export function validateConfig(value) {
|
|
|
60
60
|
fail(`upstream ${name}: choose api_key_env or api_key_command`);
|
|
61
61
|
if (up.api_key_command !== undefined && (!Array.isArray(up.api_key_command) || !up.api_key_command.length || up.api_key_command.some((arg) => typeof arg !== "string")))
|
|
62
62
|
fail(`upstream ${name}: api_key_command must be a nonempty argv array`);
|
|
63
|
-
if (up.stream_idle_timeout_ms !== undefined && (!Number.isSafeInteger(up.stream_idle_timeout_ms) || up.stream_idle_timeout_ms <= 0))
|
|
63
|
+
if (up.stream_idle_timeout_ms !== undefined && up.stream_idle_timeout_ms !== null && (!Number.isSafeInteger(up.stream_idle_timeout_ms) || up.stream_idle_timeout_ms <= 0))
|
|
64
64
|
fail(`upstream ${name}: invalid stream_idle_timeout_ms`);
|
|
65
|
+
if (up.headers_timeout_ms !== undefined && up.headers_timeout_ms !== null && (!Number.isSafeInteger(up.headers_timeout_ms) || up.headers_timeout_ms <= 0))
|
|
66
|
+
fail(`upstream ${name}: invalid headers_timeout_ms`);
|
|
65
67
|
if (up.auth_header !== undefined && (typeof up.auth_header !== "string" || !/^[\w-]+$/.test(up.auth_header)))
|
|
66
68
|
fail(`upstream ${name}: invalid auth_header`);
|
|
67
69
|
if (up.headers !== undefined && (!object(up.headers) || Object.values(up.headers).some(v => typeof v !== "string" || /[\r\n]/.test(v))))
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { createBridgeServer } from "./server.ts";
|
|
|
2
2
|
export { loadConfig, validateConfig, resolveModel } from "./config.ts";
|
|
3
3
|
export { buildCatalog, configSnippet } from "./catalog.ts";
|
|
4
4
|
export { BridgeError } from "./errors.ts";
|
|
5
|
-
export type { BridgeConfig, UpstreamConfig, ModelConfig } from "./types.ts";
|
|
5
|
+
export type { BridgeConfig, UpstreamConfig, ModelConfig, BridgeServerOptions, ResponsesRequest } from "./types.ts";
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
|
-
import type { BridgeConfig } from "./types.ts";
|
|
2
|
+
import type { BridgeConfig, BridgeServerOptions } from "./types.ts";
|
|
3
3
|
/** Creates a server without opening a socket, so callers control its lifecycle. */
|
|
4
|
-
export declare function createBridgeServer(value: BridgeConfig): Server;
|
|
4
|
+
export declare function createBridgeServer(value: BridgeConfig, options?: BridgeServerOptions): Server;
|
package/dist/server.js
CHANGED
|
@@ -71,7 +71,7 @@ function authenticated(req, token) {
|
|
|
71
71
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
72
72
|
}
|
|
73
73
|
/** Creates a server without opening a socket, so callers control its lifecycle. */
|
|
74
|
-
export function createBridgeServer(value) {
|
|
74
|
+
export function createBridgeServer(value, options = {}) {
|
|
75
75
|
const config = validateConfig(value);
|
|
76
76
|
const compactions = new CompactionHandoffStore(config, BACKENDS);
|
|
77
77
|
const catalog = JSON.stringify(buildCatalog(config));
|
|
@@ -124,7 +124,9 @@ export function createBridgeServer(value) {
|
|
|
124
124
|
}
|
|
125
125
|
res.setHeader("x-models-etag", catalogEtag);
|
|
126
126
|
const exchange = inspector.begin(req, res);
|
|
127
|
-
const
|
|
127
|
+
const original = await readRequest(req, config.request_max_mib ?? 512, exchange);
|
|
128
|
+
const requestHeaders = incomingHeaders(req.headers);
|
|
129
|
+
const request = await options.transformRequest?.(original, { headers: requestHeaders }) ?? original;
|
|
128
130
|
const caller = resolveModel(config, request.model);
|
|
129
131
|
const compacting = isCompactionRequest(request);
|
|
130
132
|
// Restore history for the actual consumer before materializing any handoffs.
|
|
@@ -139,7 +141,10 @@ export function createBridgeServer(value) {
|
|
|
139
141
|
exchange?.routed(route, responsesLite);
|
|
140
142
|
const backend = BACKENDS[route.profile.id];
|
|
141
143
|
const streaming = request.stream === true;
|
|
142
|
-
writer = new ResponseWriter(streaming ? res : { write() { } }, {
|
|
144
|
+
writer = new ResponseWriter(streaming ? res : { write() { } }, {
|
|
145
|
+
model: request.model, showSummary: reasoningSummaryRequested(request),
|
|
146
|
+
onEvent: options.onResponseEvent ? event => options.onResponseEvent(request, event) : undefined,
|
|
147
|
+
});
|
|
143
148
|
if (streaming)
|
|
144
149
|
startStream = () => {
|
|
145
150
|
if (res.headersSent || controller.signal.aborted)
|
|
@@ -155,7 +160,7 @@ export function createBridgeServer(value) {
|
|
|
155
160
|
};
|
|
156
161
|
else
|
|
157
162
|
writer.start();
|
|
158
|
-
const context = { request: executionRequest, route, signal: controller.signal, requestHeaders:
|
|
163
|
+
const context = { request: executionRequest, route, signal: controller.signal, requestHeaders, fetch: options.fetch, responsesLite, onAccepted: (headers) => {
|
|
159
164
|
for (const [name, value] of Object.entries(headers ? forwardResponseHeaders(headers) : {}))
|
|
160
165
|
res.setHeader(name, value);
|
|
161
166
|
startStream?.();
|
|
@@ -227,7 +232,13 @@ export function createBridgeServer(value) {
|
|
|
227
232
|
finally {
|
|
228
233
|
if (keepalive)
|
|
229
234
|
clearInterval(keepalive);
|
|
235
|
+
// A hook can fail after acceptance but before the backend reads the body.
|
|
236
|
+
controller.abort();
|
|
230
237
|
res.off("close", abort);
|
|
231
238
|
}
|
|
239
|
+
}).catch(() => {
|
|
240
|
+
// Failure delivery can itself throw, including from a terminal observer.
|
|
241
|
+
// Close only this response; never leave the async request unhandled.
|
|
242
|
+
res.destroy();
|
|
232
243
|
}));
|
|
233
244
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface UpstreamConfig {
|
|
|
16
16
|
/** An argv array. The command writes a short-lived credential to stdout. */
|
|
17
17
|
api_key_command?: string[];
|
|
18
18
|
auth_header?: string;
|
|
19
|
-
stream_idle_timeout_ms
|
|
19
|
+
/** Defaults to stream_idle_timeout_ms; null disables the wait-for-headers timer. */
|
|
20
|
+
headers_timeout_ms?: number | null;
|
|
21
|
+
stream_idle_timeout_ms?: number | null;
|
|
20
22
|
headers?: Record<string, string>;
|
|
21
23
|
options?: Record<string, unknown>;
|
|
22
24
|
adapters?: AdapterConfig;
|
|
@@ -110,12 +112,23 @@ export interface ResponsesRequest {
|
|
|
110
112
|
stream?: boolean;
|
|
111
113
|
[key: string]: any;
|
|
112
114
|
}
|
|
115
|
+
export interface BridgeServerOptions {
|
|
116
|
+
/** Used by every upstream call, including compaction and handoffs. */
|
|
117
|
+
fetch?: typeof globalThis.fetch;
|
|
118
|
+
/** Runs once before routing. Return a replacement or change the request in place. */
|
|
119
|
+
transformRequest?: (request: ResponsesRequest, context: {
|
|
120
|
+
headers: Headers;
|
|
121
|
+
}) => ResponsesRequest | void | Promise<ResponsesRequest | void>;
|
|
122
|
+
/** Observes committed client response events, including non-streaming responses. */
|
|
123
|
+
onResponseEvent?: (request: ResponsesRequest, event: Record<string, any>) => void;
|
|
124
|
+
}
|
|
113
125
|
export interface BackendContext {
|
|
114
126
|
request: ResponsesRequest;
|
|
115
127
|
route: ResolvedModel;
|
|
116
128
|
signal: AbortSignal;
|
|
117
129
|
/** Client metadata; transport framing and credentials are replaced upstream. */
|
|
118
130
|
requestHeaders?: Headers;
|
|
131
|
+
fetch?: typeof globalThis.fetch;
|
|
119
132
|
compacting?: boolean;
|
|
120
133
|
/** Transport mode received from the client, including tool-free Lite requests. */
|
|
121
134
|
responsesLite?: boolean;
|
package/dist/upstream.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ResolvedModel } from "./types.ts";
|
|
2
2
|
/** base_url includes any API version prefix; path is appended, never resolved from /. */
|
|
3
|
-
export declare function upstreamFetch(route: ResolvedModel, path: string, body: unknown, signal: AbortSignal, requestHeaders?: Headers, protocolHeaders?: Record<string, string | string[]
|
|
3
|
+
export declare function upstreamFetch(route: ResolvedModel, path: string, body: unknown, signal: AbortSignal, requestHeaders?: Headers, protocolHeaders?: Record<string, string | string[]>, fetch?: typeof globalThis.fetch): Promise<Response>;
|
package/dist/upstream.js
CHANGED
|
@@ -2,7 +2,7 @@ import { upstreamHeaders } from "./auth.js";
|
|
|
2
2
|
import { BridgeError, transportError, upstreamError, UpstreamHttpError } from "./errors.js";
|
|
3
3
|
import { captureUpstream } from "./inspect/capture.js";
|
|
4
4
|
/** base_url includes any API version prefix; path is appended, never resolved from /. */
|
|
5
|
-
export async function upstreamFetch(route, path, body, signal, requestHeaders, protocolHeaders) {
|
|
5
|
+
export async function upstreamFetch(route, path, body, signal, requestHeaders, protocolHeaders, fetch = globalThis.fetch) {
|
|
6
6
|
const url = route.upstream.base_url.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
|
|
7
7
|
const headers = await upstreamHeaders(route.upstream, route.profile.api, signal, requestHeaders);
|
|
8
8
|
for (const [name, value] of Object.entries(protocolHeaders ?? {})) {
|
|
@@ -16,13 +16,18 @@ export async function upstreamFetch(route, path, body, signal, requestHeaders, p
|
|
|
16
16
|
const call = captureUpstream(url, headers, payload);
|
|
17
17
|
const idle = new AbortController();
|
|
18
18
|
const combined = AbortSignal.any([signal, idle.signal]);
|
|
19
|
-
const timeoutMs = route.upstream.stream_idle_timeout_ms
|
|
19
|
+
const timeoutMs = route.upstream.stream_idle_timeout_ms === undefined ? 300_000 : route.upstream.stream_idle_timeout_ms;
|
|
20
20
|
let timer;
|
|
21
|
-
const reset = () => {
|
|
21
|
+
const reset = (duration = timeoutMs) => {
|
|
22
22
|
clearTimeout(timer);
|
|
23
|
-
timer =
|
|
23
|
+
timer = undefined;
|
|
24
|
+
if (duration === null)
|
|
25
|
+
return;
|
|
26
|
+
timer = setTimeout(() => idle.abort(new BridgeError("Upstream stream timed out", "stream_error", 504)), duration).unref();
|
|
24
27
|
};
|
|
25
|
-
|
|
28
|
+
const headersTimeout = route.upstream.headers_timeout_ms === undefined ? timeoutMs : route.upstream.headers_timeout_ms;
|
|
29
|
+
if (headersTimeout !== null)
|
|
30
|
+
reset(headersTimeout);
|
|
26
31
|
let response;
|
|
27
32
|
try {
|
|
28
33
|
response = await fetch(url, { method: "POST", headers, body: payload, signal: combined, redirect: "error" });
|
|
@@ -34,8 +39,9 @@ export async function upstreamFetch(route, path, body, signal, requestHeaders, p
|
|
|
34
39
|
throw combined.reason;
|
|
35
40
|
throw transportError(error);
|
|
36
41
|
}
|
|
37
|
-
// Downstream keepalives do not count as upstream progress.
|
|
38
|
-
//
|
|
42
|
+
// Downstream keepalives do not count as upstream progress. The body timer
|
|
43
|
+
// starts after headers, including when the header wait has no deadline.
|
|
44
|
+
reset();
|
|
39
45
|
call?.received(response);
|
|
40
46
|
if (response.body) {
|
|
41
47
|
const reader = response.body.getReader();
|
package/dist/writer.d.ts
CHANGED
|
@@ -40,10 +40,12 @@ export declare class ResponseWriter {
|
|
|
40
40
|
private nativeEnd;
|
|
41
41
|
private started;
|
|
42
42
|
private finished;
|
|
43
|
+
private onEvent?;
|
|
43
44
|
constructor(sink: Sink, opts: {
|
|
44
45
|
model: string;
|
|
45
46
|
showSummary?: boolean;
|
|
46
47
|
responseId?: string;
|
|
48
|
+
onEvent?: (event: Record<string, any>) => void;
|
|
47
49
|
});
|
|
48
50
|
get responseId(): string;
|
|
49
51
|
get nativeResponse(): Record<string, any> | undefined;
|
package/dist/writer.js
CHANGED
|
@@ -45,11 +45,13 @@ export class ResponseWriter {
|
|
|
45
45
|
nativeEnd;
|
|
46
46
|
started = false;
|
|
47
47
|
finished = false;
|
|
48
|
+
onEvent;
|
|
48
49
|
constructor(sink, opts) {
|
|
49
50
|
this.sink = sink;
|
|
50
51
|
this.model = opts.model;
|
|
51
52
|
this.showSummary = opts.showSummary ?? true;
|
|
52
53
|
this.id = opts.responseId ?? newId("resp");
|
|
54
|
+
this.onEvent = opts.onEvent;
|
|
53
55
|
}
|
|
54
56
|
get responseId() { return this.id; }
|
|
55
57
|
get nativeResponse() { return this.nativeEnd?.response; }
|
|
@@ -63,7 +65,16 @@ export class ResponseWriter {
|
|
|
63
65
|
return this.nextOutputIndex > 0 || this.calls > 0 || this.message !== undefined;
|
|
64
66
|
}
|
|
65
67
|
write(event, native = false) {
|
|
66
|
-
|
|
68
|
+
const emitted = native ? event : { sequence_number: this.seq++, ...event };
|
|
69
|
+
this.sink.write(formatSse(emitted));
|
|
70
|
+
try {
|
|
71
|
+
this.onEvent?.(emitted);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// The request's failure event must not re-enter a failed observer.
|
|
75
|
+
this.onEvent = undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
send(event, native = false) {
|
|
69
80
|
if (this.suffix)
|
|
@@ -117,8 +128,8 @@ export class ResponseWriter {
|
|
|
117
128
|
keepalive() {
|
|
118
129
|
if (!this.started || this.finished)
|
|
119
130
|
return;
|
|
120
|
-
//
|
|
121
|
-
this.write({ type: "keepalive" }
|
|
131
|
+
// Transport heartbeats bypass both the ordered suffix and the observer.
|
|
132
|
+
this.sink.write(formatSse({ ...(!this.native ? { sequence_number: this.seq++ } : {}), type: "keepalive" }));
|
|
122
133
|
}
|
|
123
134
|
/** Native Responses events retain their own ids, envelopes and metadata. */
|
|
124
135
|
useNativeResponses() { this.native = true; }
|
package/docs/config.md
CHANGED
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
| `api_key_env` | API key 环境变量名称 |
|
|
65
65
|
| `api_key_command` | 与 `api_key_env` 二选一;argv 数组,程序 stdout 返回当前凭据 |
|
|
66
66
|
| `auth_header` | 覆盖鉴权 header 名;默认 GPT/DeepSeek 用 `authorization: Bearer …`,Claude 用 `x-api-key` |
|
|
67
|
-
| `
|
|
67
|
+
| `headers_timeout_ms` | 等待上游响应头的最长时间;省略时沿用 `stream_idle_timeout_ms`,`null` 表示禁用 |
|
|
68
|
+
| `stream_idle_timeout_ms` | 收到上游响应头后,等待下一批字节的最长空闲时间,默认 300000 ms;`null` 表示禁用 |
|
|
68
69
|
| `headers` | 静态 header,覆盖同名客户端 header,例如 API 版本 |
|
|
69
70
|
| `options` | 原生请求参数,覆盖 adapter 推导出的参数 |
|
|
70
71
|
| `adapters` | 按 adapter ID 分组的桥接行为;供使用该连接的模型继承 |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 嵌入现有服务
|
|
2
|
+
|
|
3
|
+
npm 包可直接创建标准 Node HTTP server。运行已编译的包需要 Node.js ≥ 20.19;直接运行仓库里的 TypeScript 开发脚本需要 ≥ 22.18。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createBridgeServer, loadConfig } from "@wacry/codeks-bridge";
|
|
7
|
+
|
|
8
|
+
const config = await loadConfig("/absolute/path/bridge.json");
|
|
9
|
+
const server = createBridgeServer(config, {
|
|
10
|
+
async transformRequest(request, { headers }) {
|
|
11
|
+
const model = headers.get("x-example-model");
|
|
12
|
+
if (model) request.model = model;
|
|
13
|
+
},
|
|
14
|
+
fetch: (input, init) => fetch(input, init),
|
|
15
|
+
onResponseEvent(request, event) {
|
|
16
|
+
if (event.type === "response.output_item.done") {
|
|
17
|
+
console.log(request.model, event.item.type);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.listen(8123, "127.0.0.1");
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
第二个参数 `BridgeServerOptions` 可省略,不影响 CLI 和默认传输。各选项只作用于当前 server,不替换全局 fetch。
|
|
26
|
+
|
|
27
|
+
| 选项 | 调用边界 |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `transformRequest(request, { headers })` | 入口鉴权与 JSON 解析之后、模型路由和历史转换之前,主请求调用一次。可原位修改并返回 `void`,也可返回替换请求,支持 Promise。`headers` 是该请求的 HTTP headers。辅助压缩和按需复述不会再次调用它。 |
|
|
30
|
+
| `fetch` | 与标准 `fetch` 签名相同,承接全部模型请求,包括压缩、委托压缩及按需复述。可接入宿主的代理、凭据和传输策略;须保留传入的取消信号并返回标准 `Response`。 |
|
|
31
|
+
| `onResponseEvent(request, event)` | 同步观察主响应实际提交的标准 Responses 事件;`request` 是变换后的请求。缓冲后因失败丢弃的工具事件、辅助请求和 keepalive 不进入观察器。JSON 请求也产生同样的语义事件,其中每个已提交输出项有一次 `response.output_item.done`。 |
|
|
32
|
+
|
|
33
|
+
观察器应只读取参数,不修改请求或事件,也不返回 Promise。它在事件写出后同步执行;抛出的异常会进入现有请求错误处理,并停止该请求的后续观察,已经写出的事件无法撤回。如果交付终止错误时观察器自身抛错,bridge 关闭当前响应连接,不再尝试发送第二个错误;其他请求仍可继续。请求结束时取消仍在进行的上游调用,重试由客户端或宿主决定。原始上游帧及辅助调用的调试使用[检查器](inspect.md)。
|
|
34
|
+
|
|
35
|
+
宿主可通过 `AsyncLocalStorage` 在请求处理作用域中保存自己的关联信息,供 fetch 与观察器读取。bridge 不要求特定宿主元数据格式,也不管理宿主的重试。
|
|
36
|
+
|
|
37
|
+
如果宿主已有超时策略,分别设置连接的 `headers_timeout_ms` 和 `stream_idle_timeout_ms`;`null` 禁用对应阶段,省略保持默认行为,详见[连接配置](config.md#上游连接)。
|
package/docs/releasing.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 日常验证
|
|
6
6
|
|
|
7
|
-
GitHub Actions 工作流 `codeks-bridge-npm.yml` 只手动触发,不在每次提交时运行。默认 `publish=false
|
|
7
|
+
GitHub Actions 工作流 `codeks-bridge-npm.yml` 只手动触发,不在每次提交时运行。默认 `publish=false`:安装开发依赖、类型检查、测试、构建、公开信息检查,再生成 npm tarball,并在 Node.js 24 和最低运行时 20.19 上安装验证公开 API、类型、CLI、嵌入请求与 UI 资源。工作流保存实际测试过的 tarball,发布步骤直接使用同一份文件。
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
gh workflow run codeks-bridge-npm.yml --ref main
|
package/docs/responses-stream.md
CHANGED
|
@@ -87,4 +87,4 @@ GPT 保留原始 usage,包括提供的 cache write 等分项;DeepSeek 使用
|
|
|
87
87
|
|
|
88
88
|
按需复述的等待可能覆盖完整的辅助推理,客户端空闲超时应足以容纳该等待。
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
上游响应体空闲超时由 `upstreams.<id>.stream_idle_timeout_ms` 控制,默认 300 秒,也覆盖读取错误体。只有上游字节重置它,下游 keepalive 不延长它。等待响应头的时限由 `headers_timeout_ms` 控制,省略时沿用响应体空闲时限;收到响应头后重新开始响应体计时。任一字段设为 `null` 可禁用对应阶段的时限;两者均省略时保留原来的 300 秒行为。宿主已有传输重试或超时策略时,可据此分开配置,bridge 本身仍不重试。
|
package/docs/testing.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 验证
|
|
2
2
|
|
|
3
|
+
开发与直接运行 TypeScript 源码需要 Node.js ≥ 22.18;已发布 JavaScript 包支持 Node.js ≥ 20.19。
|
|
4
|
+
|
|
3
5
|
桥接器的测试只使用 Node 内置测试器和本地假上游,不需要模型密钥。普通测试不启动 codeks,也不编译其 Rust 项目。
|
|
4
6
|
|
|
5
7
|
```sh
|
|
@@ -25,6 +27,7 @@ npm run check
|
|
|
25
27
|
- 保留历史:按需复述使用客户端实际保留的历史而不再裁剪;源指令与工具声明恢复,检查点后的工作留给目标请求。
|
|
26
28
|
- 后端与历史:各 API 的消息、工具、图片、schema、thinking、usage 与错误转换;Claude 空工具结果、上下文边界停止及不完整工具不交付。
|
|
27
29
|
- 检查器:四段内容逐字节保存、只脱敏凭据 header、上游调用标签、标注与实际回放决定一致、捕获开关与结束时的软上限淘汰、清空释放数据且不打断请求、客户端断开、无 token 时的 loopback 与同源限制、token 鉴权、静态文件路径、导出格式和跨块 SSE 解析;Responses Lite 结构视图补全输出且保留原始事件。
|
|
30
|
+
- 嵌入:请求在路由前同步或异步变换;出站 fetch 按服务实例注入并覆盖三种 adapter、压缩及按需复述;观察器只接收主响应实际提交的 Responses 事件,流式与 JSON 保持一致,失败时未提交工具不外泄;响应头和响应体超时可分别禁用。
|
|
28
31
|
- Header 与错误:会话、路由及未知 header 默认透传;鉴权归所选上游,必要的传输字段移除;等待上游响应头时不发送 200;GPT HTTP status/body 与原生 SSE error 字段保留;辅助复述成功时返回目标 header,失败时返回该失败的 header;Claude/DeepSeek 继续转换协议;验证没有额外请求或自动降级。
|
|
29
32
|
|
|
30
33
|
这些测试检查协议和客户端行为;假上游不会验证模型是否理解提示词,也不会验证真实服务的缓存命中率、容量和区域之间的密文兼容性。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wacry/codeks-bridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Configurable local Responses API bridge for codeks with GPT, Claude Messages, and DeepSeek Chat adapters.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "David Zhang",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"NOTICE"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=20.19"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prepack": "npm run build",
|