@wacry/codeks-bridge 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +40 -2
- package/catalog/README.md +63 -0
- package/catalog/claude-opus-5-5.json +194 -0
- package/catalog/claude-sonnet-5-5.json +194 -0
- package/catalog/deepseek-flash.json +203 -0
- package/catalog/gpt-5.6-sol.json +152 -0
- package/catalog/gpt-6-astra.json +184 -0
- package/catalog/gpt-6-luna.json +178 -0
- package/catalog/gpt-6-sol.json +182 -0
- package/catalog/gpt-6.1-sol.json +187 -0
- package/dist/adapter-options.d.ts +5 -0
- package/dist/adapter-options.js +41 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +42 -0
- package/dist/backends/anthropic-tools.d.ts +13 -0
- package/dist/backends/anthropic-tools.js +41 -0
- package/dist/backends/anthropic.d.ts +9 -0
- package/dist/backends/anthropic.js +356 -0
- package/dist/backends/chat.d.ts +16 -0
- package/dist/backends/chat.js +284 -0
- package/dist/backends/deepseek.d.ts +6 -0
- package/dist/backends/deepseek.js +37 -0
- package/dist/backends/responses.d.ts +5 -0
- package/dist/backends/responses.js +227 -0
- package/dist/carrier.d.ts +22 -0
- package/dist/carrier.js +61 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +40 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +64 -0
- package/dist/compaction-handoff.d.ts +13 -0
- package/dist/compaction-handoff.js +126 -0
- package/dist/compaction-history.d.ts +7 -0
- package/dist/compaction-history.js +20 -0
- package/dist/compaction.d.ts +20 -0
- package/dist/compaction.js +134 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +170 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +141 -0
- package/dist/headers.d.ts +6 -0
- package/dist/headers.js +39 -0
- package/dist/history.d.ts +7 -0
- package/dist/history.js +85 -0
- package/dist/ids.d.ts +1 -0
- package/dist/ids.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/inspect/capture.d.ts +119 -0
- package/dist/inspect/capture.js +370 -0
- package/dist/inspect/http.d.ts +7 -0
- package/dist/inspect/http.js +149 -0
- package/dist/inspect/view.d.ts +239 -0
- package/dist/inspect/view.js +388 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +16 -0
- package/dist/profiles.d.ts +6 -0
- package/dist/profiles.js +63 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +118 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +232 -0
- package/dist/sse.d.ts +12 -0
- package/dist/sse.js +93 -0
- package/dist/tool-history.d.ts +4 -0
- package/dist/tool-history.js +73 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +193 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +3 -0
- package/dist/upstream.js +89 -0
- package/dist/writer.d.ts +95 -0
- package/dist/writer.js +430 -0
- package/docs/backends-anthropic.md +88 -0
- package/docs/backends-deepseek-chat.md +75 -0
- package/docs/backends-responses.md +33 -0
- package/docs/compaction.md +180 -0
- package/docs/config.md +271 -0
- package/docs/design.md +97 -0
- package/docs/inspect.md +148 -0
- package/docs/reasoning.md +72 -0
- package/docs/releasing.md +50 -0
- package/docs/responses-stream.md +90 -0
- package/docs/testing.md +177 -0
- package/docs/tools.md +68 -0
- package/examples/config.json +35 -0
- package/examples/gpt-claude.json +27 -0
- package/package.json +46 -4
- package/ui/api.js +64 -0
- package/ui/app.css +379 -0
- package/ui/app.js +489 -0
- package/ui/detail.js +713 -0
- package/ui/diff.js +281 -0
- package/ui/dom.js +258 -0
- package/ui/events.js +376 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +58 -0
- package/ui/labels.js +106 -0
- package/ui/list.js +180 -0
- package/ui/structure.js +285 -0
- package/ui/tree.js +158 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { encodeReasoning } from "../carrier.js";
|
|
2
|
+
import { BridgeError, transportError, upstreamError } from "../errors.js";
|
|
3
|
+
import { prepareHistory } from "../history.js";
|
|
4
|
+
import { parseJsonSse } from "../sse.js";
|
|
5
|
+
import { buildToolSet, normalizeToolDeclarations } from "../tools.js";
|
|
6
|
+
import { upstreamFetch } from "../upstream.js";
|
|
7
|
+
function contentParts(content) {
|
|
8
|
+
if (typeof content === "string")
|
|
9
|
+
return [{ type: "text", text: content }];
|
|
10
|
+
if (!Array.isArray(content))
|
|
11
|
+
throw new BridgeError("Chat message content must be text or a content array.");
|
|
12
|
+
return content.map((part) => {
|
|
13
|
+
if (["input_text", "output_text", "text"].includes(part?.type) && typeof part.text === "string") {
|
|
14
|
+
return { type: "text", text: part.text };
|
|
15
|
+
}
|
|
16
|
+
if (part?.type === "refusal" && typeof part.refusal === "string")
|
|
17
|
+
return { type: "text", text: part.refusal };
|
|
18
|
+
if (part?.type === "input_image" && typeof part.image_url === "string") {
|
|
19
|
+
if (!/^(https?:\/\/|data:image\/)/.test(part.image_url)) {
|
|
20
|
+
throw new BridgeError("Chat images require an HTTP(S) URL or an image data URL.");
|
|
21
|
+
}
|
|
22
|
+
return { type: "image_url", image_url: {
|
|
23
|
+
url: part.image_url,
|
|
24
|
+
...(part.detail ? { detail: part.detail === "original" ? "high" : part.detail } : {}),
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
throw new BridgeError(`Chat does not support content part ${String(part?.type)}.`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function messageContent(parts) {
|
|
31
|
+
return parts.every((p) => p.type === "text") ? parts.map((p) => p.text).join("\n") : parts;
|
|
32
|
+
}
|
|
33
|
+
function convertHistory(context, tools, reasoningField) {
|
|
34
|
+
const messages = [];
|
|
35
|
+
let assistant;
|
|
36
|
+
let pendingImages = [];
|
|
37
|
+
const flushAssistant = () => {
|
|
38
|
+
if (assistant)
|
|
39
|
+
messages.push(assistant);
|
|
40
|
+
assistant = undefined;
|
|
41
|
+
};
|
|
42
|
+
const flushImages = () => {
|
|
43
|
+
if (pendingImages.length)
|
|
44
|
+
messages.push({ role: "user", content: pendingImages });
|
|
45
|
+
pendingImages = [];
|
|
46
|
+
};
|
|
47
|
+
const getAssistant = () => assistant ??= { role: "assistant", content: "", [reasoningField]: "" };
|
|
48
|
+
if (context.request.instructions)
|
|
49
|
+
messages.push({ role: "system", content: context.request.instructions });
|
|
50
|
+
for (const item of prepareHistory(context.request.input, context.route)) {
|
|
51
|
+
if (item.type === "bridge_reasoning") {
|
|
52
|
+
flushImages();
|
|
53
|
+
// A reasoning item starts the next assistant response, even if the
|
|
54
|
+
// previous response ended without a tool or user message.
|
|
55
|
+
flushAssistant();
|
|
56
|
+
if (typeof item.state.data !== "string")
|
|
57
|
+
throw new BridgeError("Invalid Chat reasoning data.");
|
|
58
|
+
getAssistant()[reasoningField] = item.state.data;
|
|
59
|
+
}
|
|
60
|
+
else if (item.type === "message" || (!item.type && item.role)) {
|
|
61
|
+
const parts = contentParts(item.content);
|
|
62
|
+
if (item.role === "assistant") {
|
|
63
|
+
flushImages();
|
|
64
|
+
if (parts.some((p) => p.type !== "text"))
|
|
65
|
+
throw new BridgeError("Chat assistant history cannot contain images.");
|
|
66
|
+
const a = getAssistant();
|
|
67
|
+
const text = messageContent(parts);
|
|
68
|
+
a.content += (a.content && text ? "\n" : "") + text;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
flushAssistant();
|
|
72
|
+
flushImages();
|
|
73
|
+
if (!["system", "developer", "user"].includes(item.role)) {
|
|
74
|
+
throw new BridgeError(`Chat does not support message role ${String(item.role)}.`);
|
|
75
|
+
}
|
|
76
|
+
if (item.role !== "user" && parts.some((p) => p.type !== "text")) {
|
|
77
|
+
throw new BridgeError("Chat system and developer messages must contain text only.");
|
|
78
|
+
}
|
|
79
|
+
messages.push({ role: item.role === "developer" ? "system" : item.role, content: messageContent(parts) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
83
|
+
flushImages();
|
|
84
|
+
if (typeof item.call_id !== "string" || typeof item.name !== "string")
|
|
85
|
+
throw new BridgeError("Invalid Chat tool call history.");
|
|
86
|
+
const a = getAssistant();
|
|
87
|
+
a.tool_calls ??= [];
|
|
88
|
+
a.tool_calls.push({
|
|
89
|
+
id: item.call_id, type: "function", function: {
|
|
90
|
+
name: tools.flatFor(item.namespace, item.name),
|
|
91
|
+
arguments: item.type === "custom_tool_call" ? JSON.stringify({ input: item.input }) : item.arguments,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
96
|
+
flushAssistant();
|
|
97
|
+
if (typeof item.call_id !== "string")
|
|
98
|
+
throw new BridgeError("Chat tool output has no call_id.");
|
|
99
|
+
const parts = contentParts(item.output);
|
|
100
|
+
const images = parts.filter((p) => p.type === "image_url");
|
|
101
|
+
messages.push({ role: "tool", tool_call_id: item.call_id, content: messageContent(parts.filter((p) => p.type === "text")) });
|
|
102
|
+
if (images.length) {
|
|
103
|
+
pendingImages.push({ type: "text", text: `Images from tool call ${item.call_id}:` }, ...images);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw new BridgeError(`Chat does not support history item ${String(item.type)}.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
flushAssistant();
|
|
111
|
+
flushImages();
|
|
112
|
+
return messages;
|
|
113
|
+
}
|
|
114
|
+
function toolChoice(choice, tools) {
|
|
115
|
+
if (choice === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (["auto", "none", "required"].includes(choice))
|
|
118
|
+
return choice;
|
|
119
|
+
if (choice && typeof choice === "object") {
|
|
120
|
+
const selected = choice;
|
|
121
|
+
if (["function", "custom"].includes(selected.type) && typeof selected.name === "string") {
|
|
122
|
+
const tool = tools.get(selected.namespace, selected.name);
|
|
123
|
+
if (!tool || tool.kind !== selected.type) {
|
|
124
|
+
throw new BridgeError("The selected tool is not available on the Chat backend.");
|
|
125
|
+
}
|
|
126
|
+
return { type: "function", function: { name: tool.flatName } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new BridgeError("Chat does not support this tool_choice.");
|
|
130
|
+
}
|
|
131
|
+
export function buildChatRequest(context, dialect) {
|
|
132
|
+
context = { ...context, request: normalizeToolDeclarations(context.request) };
|
|
133
|
+
const { request, route } = context;
|
|
134
|
+
const tools = buildToolSet(request.tools);
|
|
135
|
+
const body = { ...dialect.requestParameters(context) };
|
|
136
|
+
const limit = request.max_output_tokens ?? route.profile.max_output_tokens;
|
|
137
|
+
if (limit !== undefined)
|
|
138
|
+
body.max_tokens = limit;
|
|
139
|
+
for (const name of ["temperature", "top_p", "parallel_tool_calls"]) {
|
|
140
|
+
if (request[name] !== undefined)
|
|
141
|
+
body[name] = request[name];
|
|
142
|
+
}
|
|
143
|
+
const choice = toolChoice(request.tool_choice, tools);
|
|
144
|
+
if (choice !== undefined)
|
|
145
|
+
body.tool_choice = choice;
|
|
146
|
+
const format = request.text?.format;
|
|
147
|
+
if (format?.type === "json_object")
|
|
148
|
+
body.response_format = { type: "json_object" };
|
|
149
|
+
else if (format?.type === "json_schema") {
|
|
150
|
+
body.response_format = { type: "json_schema", json_schema: { name: format.name, schema: format.schema, strict: format.strict } };
|
|
151
|
+
}
|
|
152
|
+
else if (format && format.type !== "text")
|
|
153
|
+
throw new BridgeError(`Chat does not support text format ${String(format.type)}.`);
|
|
154
|
+
Object.assign(body, route.upstream.options, route.config.options, {
|
|
155
|
+
model: route.config.model,
|
|
156
|
+
messages: convertHistory(context, tools, dialect.reasoningField),
|
|
157
|
+
stream: true,
|
|
158
|
+
});
|
|
159
|
+
body.stream_options = { ...body.stream_options, include_usage: true };
|
|
160
|
+
if (tools.entries.length)
|
|
161
|
+
body.tools = tools.entries.map((tool) => ({
|
|
162
|
+
type: "function", function: { name: tool.flatName, description: tool.description, parameters: tool.parameters },
|
|
163
|
+
}));
|
|
164
|
+
else
|
|
165
|
+
delete body.tools;
|
|
166
|
+
if (context.compacting) {
|
|
167
|
+
delete body.response_format;
|
|
168
|
+
}
|
|
169
|
+
return { body, tools };
|
|
170
|
+
}
|
|
171
|
+
async function* chatEvents(body, signal) {
|
|
172
|
+
try {
|
|
173
|
+
yield* parseJsonSse(body);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (signal.aborted)
|
|
177
|
+
throw signal.reason;
|
|
178
|
+
throw transportError(error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function runChat(context, writer, dialect) {
|
|
182
|
+
const { body, tools } = buildChatRequest(context, dialect);
|
|
183
|
+
const { reasoningField } = dialect;
|
|
184
|
+
const response = await upstreamFetch(context.route, "chat/completions", body, context.signal, context.requestHeaders);
|
|
185
|
+
context.onAccepted?.(response.headers);
|
|
186
|
+
if (!response.body)
|
|
187
|
+
throw new BridgeError("Chat upstream returned no stream.", "upstream_error", 502);
|
|
188
|
+
let reasoning = "";
|
|
189
|
+
let reasoningSeen = false;
|
|
190
|
+
let reasoningClosed = false;
|
|
191
|
+
let finish;
|
|
192
|
+
let usage;
|
|
193
|
+
let visible = false;
|
|
194
|
+
const calls = new Map();
|
|
195
|
+
const closeReasoning = () => {
|
|
196
|
+
if (reasoningSeen && !reasoningClosed) {
|
|
197
|
+
writer.endReasoning(encodeReasoning({ source: context.route.origin, format: context.route.profile.reasoning_format, data: reasoning }));
|
|
198
|
+
reasoningClosed = true;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
for await (const { json } of chatEvents(response.body, context.signal)) {
|
|
202
|
+
if (json.error)
|
|
203
|
+
throw upstreamError(undefined, json, response.headers);
|
|
204
|
+
if (json.usage)
|
|
205
|
+
usage = dialect.readUsage(json.usage);
|
|
206
|
+
for (const choice of json.choices ?? []) {
|
|
207
|
+
if (choice.index !== 0)
|
|
208
|
+
throw new BridgeError("Chat returned more than one completion.", "upstream_error", 502);
|
|
209
|
+
if (choice.finish_reason)
|
|
210
|
+
finish = choice.finish_reason;
|
|
211
|
+
const delta = choice.delta ?? {};
|
|
212
|
+
if (delta.refusal)
|
|
213
|
+
throw new BridgeError(String(delta.refusal));
|
|
214
|
+
if (delta[reasoningField] !== undefined && delta[reasoningField] !== null) {
|
|
215
|
+
if (typeof delta[reasoningField] !== "string" || reasoningClosed)
|
|
216
|
+
throw new BridgeError("Chat returned unsupported reasoning ordering or content.", "upstream_error", 502);
|
|
217
|
+
reasoningSeen = true;
|
|
218
|
+
reasoning += delta[reasoningField];
|
|
219
|
+
writer.reasoningDelta(delta[reasoningField]);
|
|
220
|
+
}
|
|
221
|
+
if (delta.content !== undefined && delta.content !== null && delta.content !== "") {
|
|
222
|
+
if (typeof delta.content !== "string")
|
|
223
|
+
throw new BridgeError("Chat returned non-text output.", "upstream_error", 502);
|
|
224
|
+
closeReasoning();
|
|
225
|
+
writer.textDelta(delta.content);
|
|
226
|
+
visible = true;
|
|
227
|
+
}
|
|
228
|
+
for (const part of delta.tool_calls ?? []) {
|
|
229
|
+
if (!Number.isInteger(part.index) || part.index < 0 || (part.type && part.type !== "function")) {
|
|
230
|
+
throw new BridgeError("Chat returned an invalid tool call.", "upstream_error", 502);
|
|
231
|
+
}
|
|
232
|
+
const call = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
|
|
233
|
+
if (part.id)
|
|
234
|
+
call.id += part.id;
|
|
235
|
+
if (part.function?.name)
|
|
236
|
+
call.name += part.function.name;
|
|
237
|
+
if (part.function?.arguments)
|
|
238
|
+
call.arguments += part.function.arguments;
|
|
239
|
+
calls.set(part.index, call);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!finish)
|
|
244
|
+
throw new BridgeError("Chat stream ended before a finish_reason.", "upstream_error", 502);
|
|
245
|
+
if (finish === "length")
|
|
246
|
+
throw new BridgeError("Chat output reached its token limit; increase max_output_tokens.");
|
|
247
|
+
const failure = dialect.failureReason(finish);
|
|
248
|
+
if (failure)
|
|
249
|
+
throw failure;
|
|
250
|
+
if (finish !== "stop" && finish !== "tool_calls")
|
|
251
|
+
throw new BridgeError(`Chat stopped with ${finish}.`);
|
|
252
|
+
if (!usage)
|
|
253
|
+
throw new BridgeError("Chat stream ended without token usage.", "upstream_error", 502);
|
|
254
|
+
if (finish === "tool_calls" && !calls.size)
|
|
255
|
+
throw new BridgeError("Chat ended with tool_calls but returned no calls.", "upstream_error", 502);
|
|
256
|
+
if (!visible && !calls.size)
|
|
257
|
+
throw new BridgeError("Chat returned no text or tool calls.", "upstream_error", 502);
|
|
258
|
+
closeReasoning();
|
|
259
|
+
// Validate every call before exposing any of them for execution.
|
|
260
|
+
const completed = [...calls.entries()].sort(([a], [b]) => a - b).map(([, call]) => {
|
|
261
|
+
const tool = tools.resolve(call.name);
|
|
262
|
+
if (!tool || !call.id)
|
|
263
|
+
throw new BridgeError("Chat returned an unknown or incomplete tool call.", "upstream_error", 502);
|
|
264
|
+
let args;
|
|
265
|
+
try {
|
|
266
|
+
args = JSON.parse(call.arguments);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
throw new BridgeError("Chat returned invalid tool arguments.", "upstream_error", 502);
|
|
270
|
+
}
|
|
271
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
272
|
+
throw new BridgeError("Chat tool arguments must be an object.", "upstream_error", 502);
|
|
273
|
+
if (tool.kind === "custom" && typeof args.input !== "string")
|
|
274
|
+
throw new BridgeError("Chat custom tool call has no string input.", "upstream_error", 502);
|
|
275
|
+
return { call, tool, args };
|
|
276
|
+
});
|
|
277
|
+
for (const { call, tool, args } of completed) {
|
|
278
|
+
if (tool.kind === "custom")
|
|
279
|
+
writer.addCustomToolCall({ call_id: call.id, name: tool.name, namespace: tool.namespace, input: args.input });
|
|
280
|
+
else
|
|
281
|
+
writer.addFunctionCall({ call_id: call.id, name: tool.name, namespace: tool.namespace, arguments: call.arguments });
|
|
282
|
+
}
|
|
283
|
+
return { usage, endTurn: calls.size === 0 };
|
|
284
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BridgeError } from "../errors.js";
|
|
2
|
+
import { reasoningEffort } from "../profiles.js";
|
|
3
|
+
import { buildChatRequest, runChat } from "./chat.js";
|
|
4
|
+
const deepseek = {
|
|
5
|
+
reasoningField: "reasoning_content",
|
|
6
|
+
requestParameters({ request, route }) {
|
|
7
|
+
const effort = reasoningEffort(route, request.reasoning?.effort);
|
|
8
|
+
return effort === "none"
|
|
9
|
+
? { thinking: { type: "disabled" } }
|
|
10
|
+
: { thinking: { type: "enabled" }, reasoning_effort: effort };
|
|
11
|
+
},
|
|
12
|
+
failureReason(reason) {
|
|
13
|
+
if (reason === "insufficient_system_resource" || reason === "aborted") {
|
|
14
|
+
return new BridgeError(`DeepSeek generation stopped with ${reason}.`, "server_is_overloaded", 503);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
readUsage(value) {
|
|
18
|
+
const count = (value) => {
|
|
19
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
20
|
+
throw new BridgeError("Chat stream has no valid token usage.", "upstream_error", 502);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
const cached = value?.prompt_cache_hit_tokens ?? value?.prompt_tokens_details?.cached_tokens;
|
|
25
|
+
const reasoning = value?.completion_tokens_details?.reasoning_tokens;
|
|
26
|
+
return {
|
|
27
|
+
input_tokens: count(value?.prompt_tokens),
|
|
28
|
+
output_tokens: count(value?.completion_tokens),
|
|
29
|
+
...(cached !== undefined ? { cached_tokens: count(cached) } : {}),
|
|
30
|
+
...(reasoning !== undefined ? { reasoning_tokens: count(reasoning) } : {}),
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export function buildDeepSeekRequest(context) {
|
|
35
|
+
return buildChatRequest(context, deepseek);
|
|
36
|
+
}
|
|
37
|
+
export const runDeepSeek = (context, writer) => runChat(context, writer, deepseek);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Backend, BackendContext } from "../types.ts";
|
|
2
|
+
/** Replay complete content, never a server-side item reference. */
|
|
3
|
+
export declare function responsesBody({ request, route, compacting }: BackendContext): Record<string, any>;
|
|
4
|
+
/** Transform only bridge-owned fields; native events keep their original shape. */
|
|
5
|
+
export declare const runResponses: Backend;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { encodeReasoning } from "../carrier.js";
|
|
2
|
+
import { BridgeError, ResponsesIncompleteError, responsesStreamError } from "../errors.js";
|
|
3
|
+
import { prepareHistory } from "../history.js";
|
|
4
|
+
import { reasoningEffort } from "../profiles.js";
|
|
5
|
+
import { parseJsonSse } from "../sse.js";
|
|
6
|
+
import { upstreamFetch } from "../upstream.js";
|
|
7
|
+
/** Replay complete content, never a server-side item reference. */
|
|
8
|
+
export function responsesBody({ request, route, compacting }) {
|
|
9
|
+
if (request.previous_response_id) {
|
|
10
|
+
throw new BridgeError("previous_response_id is unsupported; send the complete conversation items");
|
|
11
|
+
}
|
|
12
|
+
const input = prepareHistory(request.input, route).map((item) => {
|
|
13
|
+
// An assistant message id can reference a reasoning item that was filtered
|
|
14
|
+
// during a model switch or an agent fork. Full content needs neither id.
|
|
15
|
+
delete item.id;
|
|
16
|
+
if (item.type === "bridge_reasoning") {
|
|
17
|
+
if (typeof item.state.data !== "string")
|
|
18
|
+
throw new BridgeError("Invalid Responses reasoning data.");
|
|
19
|
+
return { type: "reasoning", summary: item.summary, encrypted_content: item.state.data };
|
|
20
|
+
}
|
|
21
|
+
return item;
|
|
22
|
+
});
|
|
23
|
+
const body = {
|
|
24
|
+
...(route.profile.max_output_tokens ? { max_output_tokens: route.profile.max_output_tokens } : {}),
|
|
25
|
+
...request,
|
|
26
|
+
reasoning: { ...request.reasoning, effort: reasoningEffort(route, request.reasoning?.effort) },
|
|
27
|
+
...route.upstream.options,
|
|
28
|
+
...route.config.options,
|
|
29
|
+
model: route.config.model,
|
|
30
|
+
input,
|
|
31
|
+
tools: request.tools,
|
|
32
|
+
store: false,
|
|
33
|
+
stream: true,
|
|
34
|
+
};
|
|
35
|
+
body.include = [...new Set([...(Array.isArray(body.include) ? body.include : []), "reasoning.encrypted_content"])];
|
|
36
|
+
delete body.previous_response_id;
|
|
37
|
+
if (input.some((item) => item.type === "compaction_trigger")) {
|
|
38
|
+
// Native compaction has its own output budget requirements. An ordinary
|
|
39
|
+
// response limit can be smaller than its provider-enforced minimum.
|
|
40
|
+
delete body.max_output_tokens;
|
|
41
|
+
}
|
|
42
|
+
else if (compacting) {
|
|
43
|
+
delete body.text;
|
|
44
|
+
}
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
function usageFrom(value) {
|
|
48
|
+
const count = (value, field) => {
|
|
49
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
50
|
+
throw new BridgeError(`Missing or invalid Responses usage field: ${field}`, "upstream_error", 502);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
// These fields are required by codeks when their enclosing objects exist.
|
|
55
|
+
count(value?.total_tokens, "total_tokens");
|
|
56
|
+
const cached = value?.input_tokens_details == null ? undefined
|
|
57
|
+
: count(value.input_tokens_details.cached_tokens, "input_tokens_details.cached_tokens");
|
|
58
|
+
const reasoning = value?.output_tokens_details == null ? undefined
|
|
59
|
+
: count(value.output_tokens_details.reasoning_tokens, "output_tokens_details.reasoning_tokens");
|
|
60
|
+
const cacheWrite = value?.input_tokens_details?.cache_write_tokens;
|
|
61
|
+
if (value?.codex_rollout_budget_units != null && !Number.isFinite(value.codex_rollout_budget_units)) {
|
|
62
|
+
throw new BridgeError("Invalid Responses usage field: codex_rollout_budget_units", "upstream_error", 502);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
input_tokens: count(value?.input_tokens, "input_tokens"),
|
|
66
|
+
output_tokens: count(value?.output_tokens, "output_tokens"),
|
|
67
|
+
...(cached !== undefined ? { cached_tokens: cached } : {}),
|
|
68
|
+
...(cacheWrite !== undefined ? { cache_write_tokens: count(cacheWrite, "input_tokens_details.cache_write_tokens") } : {}),
|
|
69
|
+
...(reasoning !== undefined ? { reasoning_tokens: reasoning } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Validate the client terminal contract before any buffered tools can run. */
|
|
73
|
+
function completedUsage(response) {
|
|
74
|
+
const invalid = (field) => {
|
|
75
|
+
throw new BridgeError(`Missing or invalid Responses completed field: ${field}`, "upstream_error", 502);
|
|
76
|
+
};
|
|
77
|
+
if (typeof response.id !== "string")
|
|
78
|
+
invalid("id");
|
|
79
|
+
if (response.end_turn != null && typeof response.end_turn !== "boolean")
|
|
80
|
+
invalid("end_turn");
|
|
81
|
+
const metadata = response.usage_metadata;
|
|
82
|
+
if (metadata != null) {
|
|
83
|
+
if (typeof metadata !== "object" || Array.isArray(metadata))
|
|
84
|
+
invalid("usage_metadata");
|
|
85
|
+
if (metadata.amount != null && typeof metadata.amount !== "string")
|
|
86
|
+
invalid("usage_metadata.amount");
|
|
87
|
+
}
|
|
88
|
+
return usageFrom(response.usage);
|
|
89
|
+
}
|
|
90
|
+
/** Transform only bridge-owned fields; native events keep their original shape. */
|
|
91
|
+
export const runResponses = async (context, writer) => {
|
|
92
|
+
writer.useNativeResponses();
|
|
93
|
+
const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined);
|
|
94
|
+
context.onAccepted?.(response.headers);
|
|
95
|
+
const states = new Map();
|
|
96
|
+
const phases = new Map();
|
|
97
|
+
let pendingPhase;
|
|
98
|
+
let held = [];
|
|
99
|
+
function itemFor(item, index) {
|
|
100
|
+
const result = { ...item };
|
|
101
|
+
if (item.type === "reasoning" && typeof item.encrypted_content === "string" && item.encrypted_content) {
|
|
102
|
+
result.encrypted_content = encodeReasoning({
|
|
103
|
+
source: context.route.origin, format: context.route.profile.reasoning_format, data: item.encrypted_content,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (item.type === "function_call") {
|
|
107
|
+
if (item.encrypted_function_args?.length) {
|
|
108
|
+
throw new BridgeError("Encrypted tool arguments are unsupported; use plaintext client tools");
|
|
109
|
+
}
|
|
110
|
+
result.encrypted_function_args ??= [];
|
|
111
|
+
}
|
|
112
|
+
if (item.type === "message" && item.phase === undefined) {
|
|
113
|
+
const phase = phases.get(index) ?? states.get(index)?.item.phase;
|
|
114
|
+
if (phase !== undefined)
|
|
115
|
+
result.phase = phase;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function responseFor(value) {
|
|
120
|
+
return {
|
|
121
|
+
...value, model: context.request.model,
|
|
122
|
+
...(Array.isArray(value.output) ? { output: value.output.map(itemFor) } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function releasePhase(phase) {
|
|
126
|
+
if (pendingPhase && phase !== undefined) {
|
|
127
|
+
pendingPhase.item.phase = phase;
|
|
128
|
+
phases.set(pendingPhase.index, phase);
|
|
129
|
+
}
|
|
130
|
+
pendingPhase = undefined;
|
|
131
|
+
const events = held;
|
|
132
|
+
held = [];
|
|
133
|
+
for (const event of events)
|
|
134
|
+
writer.nativeEvent(event);
|
|
135
|
+
}
|
|
136
|
+
function emit(event) {
|
|
137
|
+
if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
|
|
138
|
+
const index = event.output_index ?? 0;
|
|
139
|
+
if (pendingPhase && index !== pendingPhase.index && event.item.type !== "reasoning")
|
|
140
|
+
releasePhase("commentary");
|
|
141
|
+
if (event.type === "response.output_item.done" && event.item.type === "message" && event.item.phase === undefined) {
|
|
142
|
+
pendingPhase = { index, item: event.item };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (pendingPhase)
|
|
146
|
+
held.push(event);
|
|
147
|
+
else
|
|
148
|
+
writer.nativeEvent(event);
|
|
149
|
+
}
|
|
150
|
+
function outputEvent(event) {
|
|
151
|
+
const index = event.output_index ?? 0;
|
|
152
|
+
const item = itemFor(event.item, index);
|
|
153
|
+
const state = states.get(index) ?? { item, added: false, done: false };
|
|
154
|
+
if (event.type === "response.output_item.done" && !state.added) {
|
|
155
|
+
emit({ type: "response.output_item.added", output_index: index, item });
|
|
156
|
+
state.added = true;
|
|
157
|
+
}
|
|
158
|
+
state.item = item;
|
|
159
|
+
state.added ||= event.type === "response.output_item.added";
|
|
160
|
+
state.done ||= event.type === "response.output_item.done";
|
|
161
|
+
states.set(index, state);
|
|
162
|
+
emit({ ...event, item });
|
|
163
|
+
}
|
|
164
|
+
function reconcileOutput(result) {
|
|
165
|
+
// codeks persists item events, not the terminal response.output. This also
|
|
166
|
+
// applies to partial history returned only by an incomplete terminal event.
|
|
167
|
+
for (const [index, item] of (result.output ?? []).entries()) {
|
|
168
|
+
if (!states.get(index)?.done)
|
|
169
|
+
outputEvent({ type: "response.output_item.done", output_index: index, item });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function terminal(event) {
|
|
173
|
+
const result = event.response;
|
|
174
|
+
if (event.type === "response.failed" || result?.status === "failed" || result?.error) {
|
|
175
|
+
releasePhase();
|
|
176
|
+
throw responsesStreamError(responseFor(result), response.headers, event);
|
|
177
|
+
}
|
|
178
|
+
if (event.type === "response.incomplete" || result?.status === "incomplete") {
|
|
179
|
+
reconcileOutput(result);
|
|
180
|
+
releasePhase();
|
|
181
|
+
throw new ResponsesIncompleteError(responseFor(result), event);
|
|
182
|
+
}
|
|
183
|
+
if (result?.status !== "completed") {
|
|
184
|
+
throw new BridgeError("The upstream did not return a completed response", "stream_error", 502);
|
|
185
|
+
}
|
|
186
|
+
const usage = completedUsage(result);
|
|
187
|
+
reconcileOutput(result);
|
|
188
|
+
for (const state of states.values()) {
|
|
189
|
+
if (!state.done)
|
|
190
|
+
throw new BridgeError("The upstream completed with an unfinished output item", "stream_error", 502);
|
|
191
|
+
}
|
|
192
|
+
const hasCalls = [...states.values()].some(({ item }) => ["function_call", "custom_tool_call"].includes(item.type));
|
|
193
|
+
const endTurn = result.end_turn ?? !hasCalls;
|
|
194
|
+
if (pendingPhase) {
|
|
195
|
+
releasePhase(result.output?.[pendingPhase.index]?.phase ?? (endTurn ? "final_answer" : "commentary"));
|
|
196
|
+
}
|
|
197
|
+
writer.nativeEvent({ ...event, response: { ...responseFor(result), end_turn: endTurn } });
|
|
198
|
+
return { usage, endTurn };
|
|
199
|
+
}
|
|
200
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
201
|
+
const result = await response.json();
|
|
202
|
+
if (result.status === "completed" || result.status === "incomplete") {
|
|
203
|
+
writer.nativeEvent({ type: "response.created", response: { ...result, model: context.request.model, status: "in_progress", output: [] } });
|
|
204
|
+
}
|
|
205
|
+
return terminal({ type: result.status === "incomplete" ? "response.incomplete" : "response.completed", response: result });
|
|
206
|
+
}
|
|
207
|
+
if (!response.body)
|
|
208
|
+
throw new BridgeError("The upstream returned an empty response", "stream_error", 502);
|
|
209
|
+
for await (const { json: event } of parseJsonSse(response.body)) {
|
|
210
|
+
switch (event.type) {
|
|
211
|
+
case "response.output_item.added":
|
|
212
|
+
case "response.output_item.done":
|
|
213
|
+
outputEvent(event);
|
|
214
|
+
break;
|
|
215
|
+
case "response.completed":
|
|
216
|
+
case "response.failed":
|
|
217
|
+
case "response.incomplete":
|
|
218
|
+
return terminal(event);
|
|
219
|
+
case "error":
|
|
220
|
+
releasePhase();
|
|
221
|
+
throw responsesStreamError(event.error ?? event, response.headers, event);
|
|
222
|
+
default:
|
|
223
|
+
emit(event.response ? { ...event, response: responseFor(event.response) } : event);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw new BridgeError("The upstream stream ended before response.completed", "stream_error", 502);
|
|
227
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ResponsesRequest, StateOrigin } from "./types.ts";
|
|
2
|
+
export interface HandoffRecipe {
|
|
3
|
+
prefix: Record<string, any>[];
|
|
4
|
+
parameters: ResponsesRequest;
|
|
5
|
+
responsesLite: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface StatePayload {
|
|
8
|
+
source: StateOrigin;
|
|
9
|
+
format: string;
|
|
10
|
+
data: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface CompactionCarrier {
|
|
13
|
+
/** The prompt-based summary. Any model can read it. */
|
|
14
|
+
summary?: string;
|
|
15
|
+
native?: StatePayload;
|
|
16
|
+
/** Source request information needed to materialize a deferred summary. */
|
|
17
|
+
handoff?: HandoffRecipe;
|
|
18
|
+
}
|
|
19
|
+
export declare function encodeReasoning(state: StatePayload): string;
|
|
20
|
+
export declare function decodeReasoning(value: unknown): StatePayload | undefined;
|
|
21
|
+
export declare function encodeCompaction(carrier: CompactionCarrier): string;
|
|
22
|
+
export declare function decodeCompaction(value: unknown): CompactionCarrier | undefined;
|
package/dist/carrier.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BridgeError } from "./errors.js";
|
|
2
|
+
// Versioned, self-contained state stored in the client's encrypted_content field.
|
|
3
|
+
// These envelopes encode source facts and payloads, never replay policy.
|
|
4
|
+
const REASONING_PREFIX = "cxbr1:";
|
|
5
|
+
const COMPACTION_PREFIX = "cxbc1:";
|
|
6
|
+
function object(value) {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function statePayload(value) {
|
|
10
|
+
return object(value) && object(value.source) &&
|
|
11
|
+
[value.source.id, value.source.connection, value.source.model, value.source.adapter, value.format]
|
|
12
|
+
.every((field) => typeof field === "string" && field.length > 0) &&
|
|
13
|
+
Object.hasOwn(value, "data") && value.data !== undefined;
|
|
14
|
+
}
|
|
15
|
+
function compactionCarrier(value) {
|
|
16
|
+
if (!object(value) || (value.native !== undefined && !statePayload(value.native)))
|
|
17
|
+
return false;
|
|
18
|
+
if (value.summary !== undefined && (typeof value.summary !== "string" || !value.summary.trim()))
|
|
19
|
+
return false;
|
|
20
|
+
if (value.handoff !== undefined && (!object(value.handoff) ||
|
|
21
|
+
!Array.isArray(value.handoff.prefix) || !value.handoff.prefix.every(object) ||
|
|
22
|
+
!object(value.handoff.parameters) || typeof value.handoff.parameters.model !== "string" ||
|
|
23
|
+
typeof value.handoff.responsesLite !== "boolean"))
|
|
24
|
+
return false;
|
|
25
|
+
return value.summary !== undefined || (value.native !== undefined && value.handoff !== undefined);
|
|
26
|
+
}
|
|
27
|
+
function encode(prefix, value) {
|
|
28
|
+
return prefix + Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
29
|
+
}
|
|
30
|
+
function decode(value, prefix) {
|
|
31
|
+
if (typeof value !== "string" || !value.startsWith(prefix))
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(Buffer.from(value.slice(prefix.length), "base64url").toString("utf8"));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function encodeReasoning(state) {
|
|
41
|
+
if (!statePayload(state))
|
|
42
|
+
throw new TypeError("Invalid reasoning state envelope");
|
|
43
|
+
return encode(REASONING_PREFIX, state);
|
|
44
|
+
}
|
|
45
|
+
export function decodeReasoning(value) {
|
|
46
|
+
if (typeof value !== "string" || !value.startsWith(REASONING_PREFIX))
|
|
47
|
+
return undefined;
|
|
48
|
+
const decoded = decode(value, REASONING_PREFIX);
|
|
49
|
+
if (!statePayload(decoded))
|
|
50
|
+
throw new BridgeError("Malformed bridge reasoning state envelope");
|
|
51
|
+
return decoded;
|
|
52
|
+
}
|
|
53
|
+
export function encodeCompaction(carrier) {
|
|
54
|
+
if (!compactionCarrier(carrier))
|
|
55
|
+
throw new TypeError("Invalid compaction state envelope");
|
|
56
|
+
return encode(COMPACTION_PREFIX, carrier);
|
|
57
|
+
}
|
|
58
|
+
export function decodeCompaction(value) {
|
|
59
|
+
const decoded = decode(value, COMPACTION_PREFIX);
|
|
60
|
+
return compactionCarrier(decoded) ? decoded : undefined;
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BridgeConfig } from "./types.ts";
|
|
2
|
+
/** Codex bounds downloads from an explicit model_catalog_url to 1 MiB. */
|
|
3
|
+
export declare const MAX_HTTP_CATALOG_BYTES: number;
|
|
4
|
+
/** Serve client-exposed aliases, using their own reviewed client behavior. */
|
|
5
|
+
export declare function buildCatalog(config: BridgeConfig): {
|
|
6
|
+
models: Record<string, any>[];
|
|
7
|
+
};
|
|
8
|
+
export declare function configSnippet(config: BridgeConfig, catalogPath?: string): string;
|