@wacry/codeks-bridge 0.0.0-stage → 0.0.1

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.
Files changed (99) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +40 -2
  4. package/catalog/README.md +63 -0
  5. package/catalog/claude-opus-5-5.json +194 -0
  6. package/catalog/claude-sonnet-5-5.json +194 -0
  7. package/catalog/deepseek-flash.json +203 -0
  8. package/catalog/gpt-5.6-sol.json +152 -0
  9. package/catalog/gpt-6-astra.json +184 -0
  10. package/catalog/gpt-6-luna.json +178 -0
  11. package/catalog/gpt-6-sol.json +182 -0
  12. package/catalog/gpt-6.1-sol.json +187 -0
  13. package/dist/auth.d.ts +2 -0
  14. package/dist/auth.js +42 -0
  15. package/dist/backends/anthropic.d.ts +8 -0
  16. package/dist/backends/anthropic.js +309 -0
  17. package/dist/backends/chat.d.ts +16 -0
  18. package/dist/backends/chat.js +284 -0
  19. package/dist/backends/deepseek.d.ts +6 -0
  20. package/dist/backends/deepseek.js +37 -0
  21. package/dist/backends/responses.d.ts +5 -0
  22. package/dist/backends/responses.js +205 -0
  23. package/dist/carrier.d.ts +22 -0
  24. package/dist/carrier.js +61 -0
  25. package/dist/catalog.d.ts +8 -0
  26. package/dist/catalog.js +40 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +64 -0
  29. package/dist/compaction-handoff.d.ts +13 -0
  30. package/dist/compaction-handoff.js +126 -0
  31. package/dist/compaction-history.d.ts +7 -0
  32. package/dist/compaction-history.js +20 -0
  33. package/dist/compaction.d.ts +20 -0
  34. package/dist/compaction.js +134 -0
  35. package/dist/config.d.ts +6 -0
  36. package/dist/config.js +167 -0
  37. package/dist/errors.d.ts +43 -0
  38. package/dist/errors.js +141 -0
  39. package/dist/headers.d.ts +6 -0
  40. package/dist/headers.js +39 -0
  41. package/dist/history.d.ts +7 -0
  42. package/dist/history.js +85 -0
  43. package/dist/ids.d.ts +1 -0
  44. package/dist/ids.js +4 -0
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.js +4 -0
  47. package/dist/inspect/capture.d.ts +119 -0
  48. package/dist/inspect/capture.js +370 -0
  49. package/dist/inspect/http.d.ts +7 -0
  50. package/dist/inspect/http.js +149 -0
  51. package/dist/inspect/view.d.ts +239 -0
  52. package/dist/inspect/view.js +388 -0
  53. package/dist/model-catalog.d.ts +18 -0
  54. package/dist/model-catalog.js +16 -0
  55. package/dist/profiles.d.ts +6 -0
  56. package/dist/profiles.js +63 -0
  57. package/dist/prompts.d.ts +5 -0
  58. package/dist/prompts.js +118 -0
  59. package/dist/server.d.ts +4 -0
  60. package/dist/server.js +232 -0
  61. package/dist/sse.d.ts +12 -0
  62. package/dist/sse.js +93 -0
  63. package/dist/tool-history.d.ts +4 -0
  64. package/dist/tool-history.js +72 -0
  65. package/dist/tools.d.ts +33 -0
  66. package/dist/tools.js +182 -0
  67. package/dist/types.d.ts +115 -0
  68. package/dist/types.js +1 -0
  69. package/dist/upstream.d.ts +3 -0
  70. package/dist/upstream.js +84 -0
  71. package/dist/writer.d.ts +95 -0
  72. package/dist/writer.js +430 -0
  73. package/docs/backends-anthropic.md +61 -0
  74. package/docs/backends-deepseek-chat.md +75 -0
  75. package/docs/backends-responses.md +33 -0
  76. package/docs/compaction.md +176 -0
  77. package/docs/config.md +231 -0
  78. package/docs/design.md +93 -0
  79. package/docs/inspect.md +138 -0
  80. package/docs/reasoning.md +72 -0
  81. package/docs/releasing.md +50 -0
  82. package/docs/responses-stream.md +90 -0
  83. package/docs/testing.md +177 -0
  84. package/docs/tools.md +55 -0
  85. package/examples/config.json +35 -0
  86. package/package.json +46 -4
  87. package/ui/api.js +64 -0
  88. package/ui/app.css +375 -0
  89. package/ui/app.js +489 -0
  90. package/ui/detail.js +682 -0
  91. package/ui/diff.js +281 -0
  92. package/ui/dom.js +258 -0
  93. package/ui/events.js +351 -0
  94. package/ui/favicon.svg +1 -0
  95. package/ui/index.html +58 -0
  96. package/ui/labels.js +101 -0
  97. package/ui/list.js +180 -0
  98. package/ui/structure.js +285 -0
  99. package/ui/tree.js +158 -0
package/dist/writer.js ADDED
@@ -0,0 +1,430 @@
1
+ // Writes an OpenAI Responses SSE stream for codeks.
2
+ //
3
+ // Rules that codeks depends on (see docs/responses-stream.md):
4
+ // - every item gets output_item.added before any delta, and output_item.done
5
+ // with the complete item; codeks records history from the done event;
6
+ // - response.completed carries the response id and a complete usage object;
7
+ // - errors go into response.failed, never into a bare "error" event.
8
+ import { formatSse } from "./sse.js";
9
+ import { newId } from "./ids.js";
10
+ import { BridgeError } from "./errors.js";
11
+ export function usageJson(usage) {
12
+ const input = Math.max(0, Math.round(usage.input_tokens));
13
+ const output = Math.max(0, Math.round(usage.output_tokens));
14
+ const out = {
15
+ input_tokens: input,
16
+ input_tokens_details: {
17
+ cached_tokens: Math.max(0, Math.round(usage.cached_tokens ?? 0)),
18
+ ...(usage.cache_write_tokens ? { cache_write_tokens: Math.round(usage.cache_write_tokens) } : {}),
19
+ },
20
+ output_tokens: output,
21
+ total_tokens: input + output,
22
+ };
23
+ if (usage.reasoning_tokens !== undefined) {
24
+ out.output_tokens_details = { reasoning_tokens: Math.max(0, Math.round(usage.reasoning_tokens)) };
25
+ }
26
+ return out;
27
+ }
28
+ export class ResponseWriter {
29
+ id;
30
+ sink;
31
+ model;
32
+ showSummary;
33
+ seq = 0;
34
+ nextOutputIndex = 0;
35
+ output = [];
36
+ message;
37
+ reasoning;
38
+ // A finished message waits here until we know whether a tool call follows.
39
+ pendingMessage;
40
+ calls = 0;
41
+ // Once a tool appears, preserve its entire ordered suffix until success.
42
+ suffix;
43
+ suffixOutputStart = 0;
44
+ native = false;
45
+ nativeEnd;
46
+ started = false;
47
+ finished = false;
48
+ constructor(sink, opts) {
49
+ this.sink = sink;
50
+ this.model = opts.model;
51
+ this.showSummary = opts.showSummary ?? true;
52
+ this.id = opts.responseId ?? newId("resp");
53
+ }
54
+ get responseId() { return this.id; }
55
+ get nativeResponse() { return this.nativeEnd?.response; }
56
+ get outputItems() {
57
+ return this.output;
58
+ }
59
+ get isFinished() {
60
+ return this.finished;
61
+ }
62
+ get hasOutput() {
63
+ return this.nextOutputIndex > 0 || this.calls > 0 || this.message !== undefined;
64
+ }
65
+ write(event, native = false) {
66
+ this.sink.write(formatSse(native ? event : { sequence_number: this.seq++, ...event }));
67
+ }
68
+ send(event, native = false) {
69
+ if (this.suffix)
70
+ this.suffix.push({ event, native });
71
+ else
72
+ this.write(event, native);
73
+ }
74
+ emit(type, fields) {
75
+ this.send({ type, ...fields });
76
+ }
77
+ holdSuffix() {
78
+ if (this.suffix)
79
+ return;
80
+ this.suffix = [];
81
+ this.suffixOutputStart = this.output.length;
82
+ }
83
+ releaseSuffix() {
84
+ const suffix = this.suffix;
85
+ this.suffix = undefined;
86
+ for (const { event, native } of suffix ?? [])
87
+ this.write(event, native);
88
+ }
89
+ discardSuffix() {
90
+ if (this.suffix)
91
+ this.output.length = this.suffixOutputStart;
92
+ this.suffix = undefined;
93
+ this.message = undefined;
94
+ this.reasoning = undefined;
95
+ this.pendingMessage = undefined;
96
+ this.calls = 0;
97
+ }
98
+ responseObject(status, extra = {}) {
99
+ return {
100
+ id: this.responseId,
101
+ object: "response",
102
+ created_at: Math.floor(Date.now() / 1000),
103
+ model: this.model,
104
+ status,
105
+ output: status === "in_progress" ? [] : this.output,
106
+ ...extra,
107
+ };
108
+ }
109
+ start() {
110
+ if (this.started)
111
+ return;
112
+ this.started = true;
113
+ if (!this.native)
114
+ this.emit("response.created", { response: this.responseObject("in_progress") });
115
+ }
116
+ /** An event that codeks ignores but that resets its stream idle timer. */
117
+ keepalive() {
118
+ if (!this.started || this.finished)
119
+ return;
120
+ // Heartbeats bypass an ordered suffix while the upstream is still working.
121
+ this.write({ type: "keepalive" }, this.native);
122
+ }
123
+ /** Native Responses events retain their own ids, envelopes and metadata. */
124
+ useNativeResponses() { this.native = true; }
125
+ nativeEvent(event) {
126
+ if (this.finished)
127
+ return;
128
+ if (typeof event.response?.id === "string")
129
+ this.id = event.response.id;
130
+ if (event.type === "response.completed") {
131
+ this.nativeEnd = event;
132
+ return;
133
+ }
134
+ if (["response.output_item.added", "response.output_item.done"].includes(event.type)) {
135
+ if (["function_call", "custom_tool_call"].includes(event.item?.type))
136
+ this.holdSuffix();
137
+ if (event.type === "response.output_item.done")
138
+ this.output.push(event.item);
139
+ this.nextOutputIndex = Math.max(this.nextOutputIndex, Number(event.output_index ?? 0) + 1);
140
+ }
141
+ this.send(event, true);
142
+ }
143
+ nativeFailed(response, event = {}) {
144
+ this.nativeFailure("response.failed", response, event);
145
+ }
146
+ nativeIncomplete(response, event = {}) {
147
+ this.nativeFailure("response.incomplete", response, event);
148
+ }
149
+ nativeFailure(type, response, event) {
150
+ if (this.finished)
151
+ return;
152
+ this.discardSuffix();
153
+ this.finished = true;
154
+ if (typeof response.id === "string")
155
+ this.id = response.id;
156
+ this.nativeEnd = { ...event, type, response };
157
+ this.write(this.nativeEnd, true);
158
+ }
159
+ // Reasoning -----------------------------------------------------------
160
+ openReasoning() {
161
+ if (this.reasoning)
162
+ return this.reasoning;
163
+ this.closeMessage();
164
+ this.flushPendingMessage("commentary");
165
+ const r = { id: newId("rs"), outputIndex: this.nextOutputIndex++, summary: new Map() };
166
+ this.reasoning = r;
167
+ this.emit("response.output_item.added", {
168
+ output_index: r.outputIndex,
169
+ item: { type: "reasoning", id: r.id, summary: [], encrypted_content: null },
170
+ });
171
+ return r;
172
+ }
173
+ startReasoningSummary(summaryIndex = 0) {
174
+ const r = this.openReasoning();
175
+ if (!this.showSummary || r.summary.has(summaryIndex))
176
+ return;
177
+ r.summary.set(summaryIndex, { text: "", done: false });
178
+ this.emit("response.reasoning_summary_part.added", {
179
+ item_id: r.id,
180
+ output_index: r.outputIndex,
181
+ summary_index: summaryIndex,
182
+ part: { type: "summary_text", text: "" },
183
+ });
184
+ }
185
+ reasoningDelta(text, summaryIndex = 0) {
186
+ const r = this.openReasoning();
187
+ if (!text || !this.showSummary)
188
+ return;
189
+ this.startReasoningSummary(summaryIndex);
190
+ const part = r.summary.get(summaryIndex);
191
+ if (part.done)
192
+ throw new BridgeError("The upstream changed a completed reasoning summary", "stream_error", 502);
193
+ part.text += text;
194
+ this.emit("response.reasoning_summary_text.delta", {
195
+ item_id: r.id,
196
+ output_index: r.outputIndex,
197
+ summary_index: summaryIndex,
198
+ delta: text,
199
+ });
200
+ }
201
+ endReasoningSummary(summaryIndex = 0, text) {
202
+ const r = this.openReasoning();
203
+ if (!this.showSummary)
204
+ return;
205
+ this.startReasoningSummary(summaryIndex);
206
+ const part = r.summary.get(summaryIndex);
207
+ if (text !== undefined) {
208
+ if (!text.startsWith(part.text)) {
209
+ throw new BridgeError("The upstream changed already streamed reasoning summary", "stream_error", 502);
210
+ }
211
+ this.reasoningDelta(text.slice(part.text.length), summaryIndex);
212
+ }
213
+ if (part.done)
214
+ return;
215
+ part.done = true;
216
+ this.emit("response.reasoning_summary_text.done", {
217
+ item_id: r.id,
218
+ output_index: r.outputIndex,
219
+ summary_index: summaryIndex,
220
+ text: part.text,
221
+ });
222
+ this.emit("response.reasoning_summary_part.done", {
223
+ item_id: r.id,
224
+ output_index: r.outputIndex,
225
+ summary_index: summaryIndex,
226
+ part: { type: "summary_text", text: part.text },
227
+ });
228
+ }
229
+ /** Closes the open reasoning item (opening one if needed) with its carrier. */
230
+ endReasoning(encryptedContent, summary) {
231
+ const r = this.openReasoning();
232
+ for (const [index, part] of (summary ?? []).entries())
233
+ this.endReasoningSummary(index, part.text);
234
+ for (const index of r.summary.keys())
235
+ this.endReasoningSummary(index);
236
+ const item = {
237
+ type: "reasoning",
238
+ id: r.id,
239
+ summary: [...r.summary.entries()].sort(([a], [b]) => a - b)
240
+ .map(([, part]) => ({ type: "summary_text", text: part.text })),
241
+ encrypted_content: encryptedContent ?? null,
242
+ };
243
+ this.reasoning = undefined;
244
+ this.emit("response.output_item.done", { output_index: r.outputIndex, item });
245
+ this.output.push(item);
246
+ }
247
+ get reasoningOpen() {
248
+ return this.reasoning !== undefined;
249
+ }
250
+ // Messages ------------------------------------------------------------
251
+ textDelta(text, phase) {
252
+ if (this.message && phase !== undefined)
253
+ this.message.phase = phase;
254
+ if (!text)
255
+ return;
256
+ if (this.reasoning)
257
+ this.endReasoning(undefined);
258
+ if (!this.message) {
259
+ this.flushPendingMessage("commentary");
260
+ const m = { id: newId("msg"), outputIndex: this.nextOutputIndex++, text: "", phase };
261
+ this.message = m;
262
+ this.emit("response.output_item.added", {
263
+ output_index: m.outputIndex,
264
+ item: { type: "message", id: m.id, role: "assistant", status: "in_progress", content: [], ...(phase !== undefined ? { phase } : {}) },
265
+ });
266
+ this.emit("response.content_part.added", {
267
+ item_id: m.id,
268
+ output_index: m.outputIndex,
269
+ content_index: 0,
270
+ part: { type: "output_text", text: "", annotations: [] },
271
+ });
272
+ }
273
+ this.message.text += text;
274
+ this.emit("response.output_text.delta", {
275
+ item_id: this.message.id,
276
+ output_index: this.message.outputIndex,
277
+ content_index: 0,
278
+ delta: text,
279
+ });
280
+ }
281
+ /** Ends the current text block. Its done event waits until the phase is known. */
282
+ closeMessage(phase) {
283
+ if (!this.message)
284
+ return;
285
+ if (phase !== undefined)
286
+ this.message.phase = phase;
287
+ this.flushPendingMessage("commentary");
288
+ this.pendingMessage = this.message;
289
+ this.message = undefined;
290
+ }
291
+ flushPendingMessage(phase) {
292
+ const m = this.pendingMessage;
293
+ if (!m)
294
+ return;
295
+ this.pendingMessage = undefined;
296
+ const item = {
297
+ type: "message",
298
+ id: m.id,
299
+ role: "assistant",
300
+ status: "completed",
301
+ content: [{ type: "output_text", text: m.text, annotations: [] }],
302
+ phase: m.phase ?? phase,
303
+ };
304
+ this.emit("response.output_text.done", {
305
+ item_id: m.id,
306
+ output_index: m.outputIndex,
307
+ content_index: 0,
308
+ text: m.text,
309
+ });
310
+ this.emit("response.output_item.done", { output_index: m.outputIndex, item });
311
+ this.output.push(item);
312
+ }
313
+ // Tool calls (held until the upstream response finishes) ---------------
314
+ addFunctionCall(call) {
315
+ this.closeMessage();
316
+ if (this.reasoning)
317
+ this.endReasoning(undefined);
318
+ const item = {
319
+ type: "function_call",
320
+ id: newId("fc"),
321
+ call_id: call.call_id,
322
+ name: call.name,
323
+ arguments: call.arguments,
324
+ encrypted_function_args: [],
325
+ status: "completed",
326
+ };
327
+ if (call.namespace)
328
+ item.namespace = call.namespace;
329
+ this.addTool(item);
330
+ }
331
+ addCustomToolCall(call) {
332
+ this.closeMessage();
333
+ if (this.reasoning)
334
+ this.endReasoning(undefined);
335
+ this.addTool({
336
+ type: "custom_tool_call",
337
+ id: newId("ctc"),
338
+ call_id: call.call_id,
339
+ name: call.name,
340
+ input: call.input,
341
+ ...(call.namespace ? { namespace: call.namespace } : {}),
342
+ status: "completed",
343
+ });
344
+ }
345
+ addTool(item) {
346
+ this.flushPendingMessage("commentary");
347
+ this.holdSuffix();
348
+ this.calls++;
349
+ const outputIndex = this.nextOutputIndex++;
350
+ const added = { ...item, status: "in_progress" };
351
+ if (item.type === "function_call")
352
+ added.arguments = "";
353
+ else
354
+ added.input = "";
355
+ this.emit("response.output_item.added", { output_index: outputIndex, item: added });
356
+ this.emit("response.output_item.done", { output_index: outputIndex, item });
357
+ this.output.push(item);
358
+ }
359
+ get toolCallCount() {
360
+ return this.output.slice(0, this.suffix ? this.suffixOutputStart : undefined)
361
+ .filter((item) => ["function_call", "custom_tool_call"].includes(item.type)).length;
362
+ }
363
+ /** Emits a complete item as-is (added and done together). */
364
+ addItem(item) {
365
+ this.closeMessage();
366
+ this.flushPendingMessage("commentary");
367
+ if (this.reasoning)
368
+ this.endReasoning(undefined);
369
+ const outputIndex = this.nextOutputIndex++;
370
+ this.emit("response.output_item.added", { output_index: outputIndex, item });
371
+ this.emit("response.output_item.done", { output_index: outputIndex, item });
372
+ this.output.push(item);
373
+ }
374
+ flushAll(endTurn) {
375
+ this.closeMessage();
376
+ if (this.reasoning)
377
+ this.endReasoning(undefined);
378
+ this.flushPendingMessage(endTurn === false || this.calls > 0 ? "commentary" : "final_answer");
379
+ }
380
+ // Endings ---------------------------------------------------------------
381
+ complete(usage, endTurn) {
382
+ if (this.finished)
383
+ return;
384
+ this.start();
385
+ if (this.nativeEnd) {
386
+ this.releaseSuffix();
387
+ this.finished = true;
388
+ this.write(this.nativeEnd, true);
389
+ return;
390
+ }
391
+ if (this.native)
392
+ throw new BridgeError("The upstream stream ended before response.completed", "stream_error", 502);
393
+ this.flushAll(endTurn);
394
+ this.releaseSuffix();
395
+ this.finished = true;
396
+ const extra = {};
397
+ if (usage)
398
+ extra.usage = usageJson(usage);
399
+ if (endTurn !== undefined)
400
+ extra.end_turn = endTurn;
401
+ this.emit("response.completed", { response: this.responseObject("completed", extra) });
402
+ }
403
+ /** Fails the response. Tool calls that were not emitted yet are dropped. */
404
+ fail(error) {
405
+ if (this.finished)
406
+ return;
407
+ this.start();
408
+ this.discardSuffix();
409
+ this.finished = true;
410
+ this.emit("response.failed", {
411
+ response: this.responseObject("failed", { error }),
412
+ });
413
+ }
414
+ incomplete(reason, usage) {
415
+ if (this.finished)
416
+ return;
417
+ this.start();
418
+ if (this.suffix)
419
+ this.discardSuffix();
420
+ this.closeMessage();
421
+ if (this.reasoning)
422
+ this.endReasoning(undefined);
423
+ this.flushPendingMessage("final_answer");
424
+ this.finished = true;
425
+ const extra = { incomplete_details: { reason } };
426
+ if (usage)
427
+ extra.usage = usageJson(usage);
428
+ this.emit("response.incomplete", { response: this.responseObject("incomplete", extra) });
429
+ }
430
+ }
@@ -0,0 +1,61 @@
1
+ # Claude Messages
2
+
3
+ 模型选择 `catalog: "claude-opus-5-5"` 或 `"claude-sonnet-5-5"`,条目指定 `claude-messages` adapter,请求转换为 `POST <base_url>/messages`。官方端点的 `base_url` 为 `https://api.anthropic.com/v1`;默认使用 `x-api-key` 和 `anthropic-version: 2023-06-01`,可通过连接配置覆盖。
4
+
5
+ ## 历史与 thinking
6
+
7
+ 开头的 `instructions`、system 和 developer 消息组成顶层 `system`。会话中途的 system/developer 消息留在原位置,转换成 user 文本中的 `<system-reminder>`。新指令不会搬到会话顶部,因此已有前缀稳定;这保留时间顺序,但不具有原生 system 角色的优先级。
8
+
9
+ 相邻同角色消息合并;reasoning、文本和工具调用成为 assistant 内容块,工具输出成为下一条 user 消息中的 `tool_result`。原历史不变。
10
+
11
+ 默认 effort 为 `medium`,普通映射提供 `low`、`medium`、`high`、`xhigh`、`max` 并保留同名值,使用 adaptive thinking 并写入 `output_config.effort`。codeks 的 `ultra` 注入多代理提示词,并将 Opus 5.5 的原生 effort 设为 `xhigh`、Sonnet 5.5 设为 `max`。
12
+
13
+ Opus 5.5 始终启用 thinking;Sonnet 5.5 的最低模式是 `between_tools`,关闭回复前的思考,但仍在工具之间返回带签名的进度说明。两者都拒绝 `thinking.type: "disabled"`,因此目录不把 `none` 作为可选档位,也不把关闭思考的请求静默改成 `low`。Sonnet 的 `between_tools` 只接受 `high` 或以下 effort,且 `thinking` 对象中不能附带 `display`、`budget_tokens` 等字段。[官方 thinking 规则](https://platform.claude.com/docs/en/build-with-claude/thinking)、[Sonnet 5.5 变更说明](https://platform.claude.com/docs/en/models/sonnet-5-5/whats-new-sonnet-5-5)
14
+
15
+ `reasoning.summary` 缺省或为 `none` 时选择 `display: "omitted"`;`auto`、`concise`、`detailed` 选择 `summarized`,不保证上游区分摘要长度。目录默认值与 GPT-6.1 Sol 一样为 `none`;要显示摘要,在 codeks 配置中设置 `model_reasoning_summary = "auto"`。显示开关不关闭 thinking 或载荷回放。
16
+
17
+ `reasoning.effort_map` 可以替换映射,`options.thinking` 和 `options.output_config` 可以覆盖完整原生对象。例如以下 Sonnet 模型条目固定使用 `between_tools` 与 `low` effort;options 优先于客户端请求中推导的模式、effort 和 display,无需新增转换接口:
18
+
19
+ ```json
20
+ {
21
+ "catalog": "claude-sonnet-5-5",
22
+ "upstream": "anthropic",
23
+ "model": "claude-sonnet-5-5",
24
+ "options": {
25
+ "thinking": { "type": "between_tools" },
26
+ "output_config": { "effort": "low" }
27
+ }
28
+ }
29
+ ```
30
+
31
+ 未映射的显式 effort 仍交给上游判断;客户端显式请求 `none` 时,adapter 仍发送 `thinking.type: "disabled"`,由上游返回错误。目录精调不构成运行时能力预检。
32
+
33
+ Opus / Sonnet 5.5 目录的默认及最大上下文窗口为 `1000000`,Messages API 要求提供 `max_tokens`,因此目录设置默认输出预算 `128000`,对应官方标准最大输出。输出预算依次取请求 `max_output_tokens`、模型配置、目录默认值,再接受上游和模型 options 的覆盖;这是允许的上限,不要求每次生成同样长度。[模型规格](https://platform.claude.com/docs/en/models/sonnet-5-5/overview)、[配置优先级](config.md)
34
+
35
+ 完整 `thinking`/`redacted_thinking` block,包括签名或隐藏数据,存入 `anthropic.thinking.v1` carrier。请求按 `replay.reasoning_from` 恢复允许来源,签名完整保留;其他来源只在当前请求过滤。允许配对不意味着上游一定接受其签名。[Reasoning 策略](reasoning.md)
36
+
37
+ ## 输出格式、工具与图片
38
+
39
+ Responses `text.format: {type: "json_schema", schema: ...}` 转为 `output_config.format: {type: "json_schema", schema: ...}`;name/strict 不转发。schema 内容直接交给上游,随后 options 仍可覆盖原生格式。普通 `text` 不添加约束;无法表达为 Messages 的未知格式报错。
40
+
41
+ namespace 和 custom tool 使用共享映射。自由文本工具转换成 `{input: string}`,Lark/regex 放入描述,返回时恢复原始字符串;不添加工具专属提示词。指定工具必须能在当前工具表中解析,`tool_choice: "required"` 转为原生 `any`,不会改成 `auto`。显式 `temperature`、`top_p` 也继续发送;这些参数与具体模型、thinking 的组合是否受支持由上游判断。静默删除参数或关闭 thinking 会改变用户请求,bridge 不做这类修正。
42
+
43
+ 有工具且未选择 `none` 时,显式 `parallel_tool_calls: false` 转为 `tool_choice.disable_parallel_tool_use: true`,显式 `true` 则转为 `false`;未指定 choice 时采用 `auto`,原生 options 仍可覆盖。这保留 Codex Lite 的外层工具调用限制,不限制一次 `exec` 内 JavaScript 的并发执行。[Claude 并行工具调用](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use)
44
+
45
+ 用户与工具结果都可带图片。HTTP(S) URL 和 JPEG、PNG、GIF、WebP 的 base64 data URL 转为原生 image block,bridge 不下载图片。图片能力、尺寸和 schema 限制交给上游,不能转换的内容类型明确报错。空工具结果仍发送 `tool_result` 及原调用 ID,只省略空的 `content` 数组,不添加占位文字。
46
+
47
+ 工具参数分片先拼接、解析,在对应 `content_block_stop` 按原顺序交给 writer 缓存,完整 `message_stop` 后才交付客户端。工具之后的 thinking 和文字一起缓冲,保留签名所依赖的块顺序。`tool_use`、`pause_turn` 返回 `end_turn: false`;输出截断或 refusal 返回失败,不交付部分工具调用。`model_context_window_exceeded` 转为 `context_length_exceeded`,同样不交付工具调用;后续是否压缩或重试由 codeks 决定。
48
+
49
+ ## 缓存与压缩
50
+
51
+ 最多四个显式缓存断点:最后一个工具、最后一段顶层 system、最近两条 user 消息的末块。普通请求 TTL 为 `1h`。压缩保留公共前缀的 `1h`,只将最后的摘要请求末块设为 `5m`,使长 TTL 位于短 TTL 之前。thinking 块不直接设置缓存断点。
52
+
53
+ 压缩只生成文字摘要,保留工具定义及普通请求的工具选择,移除 `output_config.format`,其他参数保留。摘要封装为客户端 V2 compaction item,可供其他模型恢复。保留工具选择的缓存依据及意外工具调用的处理见 [压缩](compaction.md#工具选择与缓存)。
54
+
55
+ ## Usage 与错误
56
+
57
+ input 为 `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`,output 使用最终累计值,不累加流事件;可得的 thinking 用量记录到 reasoning 分项。缺失或无效的计数返回错误,不伪造零值。
58
+
59
+ HTTP 与流内错误经共享层分类,同时保留上游原始错误体;未完整结束的流返回错误。bridge 不改参数重试,容量与限流退避由 codeks 决定。[错误结构](responses-stream.md)
60
+
61
+ 实现与测试:[`src/backends/anthropic.ts`](https://github.com/WAcry/codeks-bridge/blob/main/src/backends/anthropic.ts)、[`test/anthropic.test.ts`](https://github.com/WAcry/codeks-bridge/blob/main/test/anthropic.test.ts)。协议参考:[thinking](https://platform.claude.com/docs/en/build-with-claude/thinking)、[结构化输出](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)、[缓存](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)。
@@ -0,0 +1,75 @@
1
+ # DeepSeek Chat
2
+
3
+ 模型设置 `catalog: "deepseek-flash"`,条目指定 `deepseek-chat` adapter,请求发送到 `<base_url>/chat/completions`。这个 adapter 明确使用 DeepSeek 方言:`reasoning_content`、thinking 参数和对应 usage。实际模型可以是 `deepseek-flash` 或第三方部署名称,bridge 不按名字猜测规则。
4
+
5
+ ```json
6
+ {
7
+ "upstreams": {
8
+ "deepseek": {
9
+ "base_url": "https://api.deepseek.com",
10
+ "api_key_env": "DEEPSEEK_API_KEY"
11
+ }
12
+ },
13
+ "models": {
14
+ "flash": {
15
+ "upstream": "deepseek",
16
+ "catalog": "deepseek-flash",
17
+ "model": "deepseek-flash"
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ 目录的默认及最大上下文窗口为 `1000000`。输出预算使用上游默认值;请求中的 `max_output_tokens` 或模型配置可指定预算,请求值优先,并转为 `max_tokens`;原生 options 仍可覆盖。上游的输出预算包含 thinking 与可见输出;达到上限返回失败,不执行不完整工具调用。`base_url` 可包含 `/v1` 等前缀。[官方参数范围](https://api-docs.deepseek.com/api/create-chat-completion/)
24
+
25
+ ## Reasoning 与参数
26
+
27
+ 思考从 `delta.reasoning_content` 读取,作为 `reasoning.text.v1` 的字符串 data 保存。下次请求把允许来源的文本恢复到 assistant 的 `reasoning_content`。`reasoning.summary` 为 `auto`、`concise` 或 `detailed` 时,将原生 reasoning 文本作为可见摘要输出;缺省或 `none` 时不展示,但仍保存和回放载荷,不关闭 thinking。目录沿用 GPT-6.1 Sol 的摘要默认值 `none`;可在 codeks 中设置 `model_reasoning_summary = "auto"`。是否同模型、是否同上游都由来源策略决定,不能只凭文本格式推断应该共享。[复用配置](reasoning.md)
28
+
29
+ 没有可恢复 reasoning 的 assistant 历史使用 `reasoning_content: ""`,不编造思考。它使带有其他目标历史的工具循环仍具备 DeepSeek 要求的字段。
30
+
31
+ 默认 effort 为 `high`,默认映射如下:
32
+
33
+ | 客户端 effort | 原生参数 |
34
+ | --- | --- |
35
+ | `none` | `thinking.type: "disabled"` |
36
+ | `minimal`、`low` | `thinking.type: "enabled"`,`reasoning_effort: "low"` |
37
+ | `medium`、`high`、`xhigh` | `thinking.type: "enabled"`,`reasoning_effort: "high"` |
38
+ | `max`、`ultra` | `thinking.type: "enabled"`,`reasoning_effort: "max"` |
39
+
40
+ codeks 的 `ultra` 由客户端注入多代理提示词,并使用原生 `max` effort。映射可由 `reasoning.effort_map` 整体替换,目录中可选档位来自同一配置。未映射的显式 effort 原样发送。上游和模型 `options` 随后覆盖推导参数,允许直接设置端点专有选项;桥接器最终固定 model、messages、工具定义、流模式和 usage 开关。
41
+
42
+ 强制工具选择、JSON Schema 与 thinking 能否组合由上游判断。bridge 转换可表达的请求后直接发送,上游不支持时保留错误,不自动关闭 thinking 或改成普通文本输出。
43
+
44
+ ## 消息、工具与图片
45
+
46
+ Responses instructions 成为开头的 system 消息;历史 developer 在原位置转为 system。user、assistant、工具调用和结果保留顺序。同一次 assistant 输出的 reasoning、文字和并行工具调用合并为一条 Chat assistant 消息。
47
+
48
+ 工具结果使用 `role: "tool"` 和原 `tool_call_id`。namespace、自由文本工具及语法使用[共享工具转换](tools.md)。Responses JSON Schema 转为 Chat `response_format.json_schema`,JSON object 转为 `response_format: {type: "json_object"}`;schema 由上游验证。
49
+
50
+ 用户图片转为 `image_url`,支持 HTTP(S) 和 image data URL,`detail: "original"` 转为 `high`。工具结果文本保持 tool 消息,图片放在同组所有 tool 结果之后的 user 消息,标注来源 call ID;这样不打断并行调用与结果的配对。这会增加一个 user 边界,是转换的代价。
51
+
52
+ assistant/system/developer 图片、文件 ID、音频和视频没有本轮转换路径,明确报错。图片的实际识别能力和尺寸限制交给模型端点。
53
+
54
+ ## 流、用量与压缩
55
+
56
+ 出站开启 `stream: true`、`stream_options.include_usage: true`。文字和启用展示的 reasoning 随到随发,工具参数按索引拼接,只有流结束且所有调用完整时才交给 codeks。未结束的连接、缺失 usage、未知工具或不完整参数不能构成成功响应。
57
+
58
+ `stop` 完成,`tool_calls` 要求继续执行工具。`length` 和内容过滤返回错误;DeepSeek 的 `insufficient_system_resource`、`aborted` 归为容量错误。HTTP 与流内错误保留原始上游详情,bridge 不叠加重试。[错误结构](responses-stream.md)
59
+
60
+ | DeepSeek 用量 | Responses 用量 |
61
+ | --- | --- |
62
+ | `prompt_tokens` | `input_tokens` |
63
+ | `completion_tokens` | `output_tokens` |
64
+ | `prompt_cache_hit_tokens`,或 `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` |
65
+ | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.reasoning_tokens` |
66
+
67
+ reasoning 不重复加到总量中。压缩只生成文字摘要,仍由客户端远程 V2 通道接收;保留工具定义及普通请求的工具选择。参数保持与意外工具调用的处理见 [压缩](compaction.md#工具选择与缓存)。
68
+
69
+ ## 扩展方言
70
+
71
+ [`src/backends/chat.ts`](https://github.com/WAcry/codeks-bridge/blob/main/src/backends/chat.ts) 负责 Chat 消息、工具和流转换,定义小型 `ChatDialect` 接口;[`src/backends/deepseek.ts`](https://github.com/WAcry/codeks-bridge/blob/main/src/backends/deepseek.ts) 提供 DeepSeek 的 reasoning 字段、thinking 参数、usage 和特殊结束原因。`deepseek-flash` 目录条目选择 `deepseek-chat` adapter,由后者调用前者。
72
+
73
+ 当前 Chat 转换层按单个文本字段读写 reasoning,包括为没有 reasoning 的 assistant 历史写入空字符串。这是现有 DeepSeek 路径需要的行为;接入其他模型时须按其协议决定能否复用。参数差异优先使用 profile 和 options;如果 reasoning 改为内容块或其他结构,再调整转换边界。当前没有其他 Chat 方言实现。
74
+
75
+ 验证见 [`test/chat.test.ts`](https://github.com/WAcry/codeks-bridge/blob/main/test/chat.test.ts)。协议参考:[Chat API](https://api-docs.deepseek.com/zh-cn/api/create-chat-completion)、[思考模式](https://api-docs.deepseek.com/zh-cn/guides/thinking_mode)、[图片](https://api-docs.deepseek.com/zh-cn/guides/vision)。
@@ -0,0 +1,33 @@
1
+ # GPT Responses
2
+
3
+ 模型选择 GPT 目录 key,例如 `catalog: "gpt-6.1-sol"`,条目指定 `gpt-responses` adapter,bridge 向连接的 `/responses` 发请求。`base_url` 包含版本前缀,例如 `https://api.openai.com/v1`。实际模型名可以是任意部署别名,转换不依赖名称前缀。
4
+
5
+ ## 请求与参数
6
+
7
+ 普通 Responses 参数、namespace 和 custom grammar 工具保留原始格式,包括未知扩展字段;请求及响应 header 按 [HTTP 边界](responses-stream.md#header-与转发边界) 默认透传。模型名替换为配置的实际名称,reasoning effort 使用 profile 默认值和配置映射。`max_output_tokens` 优先使用请求值,再使用模型默认值。
8
+
9
+ 客户端默认发送 Responses Lite:工具声明保留在 `input[].additional_tools` 的原位置,不提升到顶层 `tools`。保留 `reasoning.context: "all_turns"` 和 `parallel_tool_calls: false`,并向所有 GPT 上游发送 Lite header。入口从客户端 header 或 `additional_tools` 识别 Lite;只有 header、没有工具的请求也能保持标记。后端不通过模型名称或云厂商猜测协议。
10
+
11
+ 随后浅层合并上游和模型 `options`,后者覆盖前者,也可覆盖推导出的普通参数。最后由 bridge 固定 `model`、`input`、`tools`、`store: false`、`stream: true`,`include` 至少包含 `reasoning.encrypted_content`。上游状态由完整历史承载,不依赖服务器保存的会话。
12
+
13
+ 历史 reasoning 按允许来源选择,将 `responses.reasoning.v1` 的 data 还原为原生密文;原生压缩按独立策略选择。每条输入项移除顶层 `id`,工具配对的 `call_id` 保留,避免引用已过滤的 reasoning。图片、普通消息、工具结果和明文 agent message 保留原格式。[状态规则](reasoning.md)
14
+
15
+ ## 流与工具交付
16
+
17
+ 桥接器消费 SSE,也接受完整 JSON response。原生事件和 response envelope 保留 ID、时间、usage 和未知元数据,只修改桥接所需字段。完整 JSON 或仅在终止事件提供的输出会补齐 item added/done,供客户端持久化。保留上游消息的 `phase`,包括 `partial_answer`;只有缺失时才按后续输出、工具和 `end_turn` 推断。摘要保留同一 item 内的分段索引及完整 summary 数组,不把多段压成一段。
18
+
19
+ 第一个工具之前的文字和摘要流式交付;从第一个工具起,所有事件作为有序后缀缓存,完整成功后释放。function call 保留 namespace,custom tool 保留原始输入。失败、截断和连接中断丢弃整个后缀,不让客户端执行未完成的工具调用,也不改变工具与后续消息的相对顺序。
20
+
21
+ `end_turn` 有显式值时遵从上游,否则有工具调用时返回 false。成功响应的 input/output usage 必须是有效计数,cache read/write、reasoning 分项有则记录;reasoning 不重复叠加到 input。无效 usage 作为错误返回,避免影响 codeks 压缩判断。`response.incomplete` 原样保留原因与可用 usage,由客户端处理继续生成、内容过滤或截断,不统一改成可重试错误。
22
+
23
+ 上游成功响应头到达前,不建立下游 SSE。HTTP 失败保留原始 status、body 和诊断 header;流内错误保留完整 error 对象,不维护 GPT code 白名单。被允许的密文遭拒也直接返回,不自动过滤后重试。[错误结构](responses-stream.md)
24
+
25
+ ## 原生压缩
26
+
27
+ 默认 `compaction.mode: "native"` 向 `/responses` 发送保留 `compaction_trigger` 的请求,保存原生检查点及复述配方。目标需要文字表示时,原来源读取后续请求中实际保留的历史与原生检查点,使用 handoff prompt 复述,并缓存成功结果。若端点只适合文字压缩,显式设置 `mode: "summary"`,使用标准 prompt 汇总原历史。
28
+
29
+ 原生请求移除普通回答的 `max_output_tokens`,包括配置和 options 中的值,由上游管理压缩输出预算。摘要仍使用普通预算,保留工具定义和工具选择,移除结构化输出格式;工具调用的失败语义见 [压缩](compaction.md#工具选择与缓存)。
30
+
31
+ 是否生成原生压缩与是否恢复原生检查点独立。配置 `native_replay` 和 `native_compaction_from` 后,格式可读且来源允许的原生状态优先发送;上游拒绝即报错。[压缩配置](compaction.md)
32
+
33
+ 实现与测试:[`src/backends/responses.ts`](https://github.com/WAcry/codeks-bridge/blob/main/src/backends/responses.ts)、[`test/responses.test.ts`](https://github.com/WAcry/codeks-bridge/blob/main/test/responses.test.ts)。