@velum-labs/routekit-gateway 0.9.0
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 +201 -0
- package/README.md +28 -0
- package/dist/acp-agent.d.ts +38 -0
- package/dist/acp-agent.js +142 -0
- package/dist/acp-registry.d.ts +36 -0
- package/dist/acp-registry.js +85 -0
- package/dist/adapters/anthropic.d.ts +131 -0
- package/dist/adapters/anthropic.js +1195 -0
- package/dist/adapters/chat.d.ts +14 -0
- package/dist/adapters/chat.js +34 -0
- package/dist/adapters/cursor.d.ts +34 -0
- package/dist/adapters/cursor.js +305 -0
- package/dist/adapters/dropped.d.ts +10 -0
- package/dist/adapters/dropped.js +24 -0
- package/dist/adapters/openai-chat-wire.d.ts +93 -0
- package/dist/adapters/openai-chat-wire.js +143 -0
- package/dist/adapters/responses-stream.d.ts +7 -0
- package/dist/adapters/responses-stream.js +597 -0
- package/dist/adapters/responses.d.ts +174 -0
- package/dist/adapters/responses.js +778 -0
- package/dist/adapters/server-tool-loop.d.ts +94 -0
- package/dist/adapters/server-tool-loop.js +477 -0
- package/dist/adapters/upstream-error.d.ts +14 -0
- package/dist/adapters/upstream-error.js +25 -0
- package/dist/adapters/validate.d.ts +27 -0
- package/dist/adapters/validate.js +180 -0
- package/dist/adapters/web-search.d.ts +46 -0
- package/dist/adapters/web-search.js +151 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +28 -0
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +143 -0
- package/dist/capacity-pool.d.ts +31 -0
- package/dist/capacity-pool.js +99 -0
- package/dist/cost.d.ts +49 -0
- package/dist/cost.js +112 -0
- package/dist/endpoint-health.d.ts +54 -0
- package/dist/endpoint-health.js +123 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +23 -0
- package/dist/provenance.d.ts +31 -0
- package/dist/provenance.js +191 -0
- package/dist/provider-backends.d.ts +40 -0
- package/dist/provider-backends.js +1050 -0
- package/dist/provider-source.d.ts +40 -0
- package/dist/provider-source.js +293 -0
- package/dist/router.d.ts +168 -0
- package/dist/router.js +474 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +930 -0
- package/dist/sse/chat-assembler.d.ts +45 -0
- package/dist/sse/chat-assembler.js +190 -0
- package/dist/sse/parse.d.ts +50 -0
- package/dist/sse/parse.js +149 -0
- package/dist/sse-wire.d.ts +10 -0
- package/dist/sse-wire.js +31 -0
- package/dist/switching-proxy.d.ts +15 -0
- package/dist/switching-proxy.js +232 -0
- package/dist/test/acp-agent.test.d.ts +1 -0
- package/dist/test/acp-agent.test.js +66 -0
- package/dist/test/acp-registry.test.d.ts +1 -0
- package/dist/test/acp-registry.test.js +70 -0
- package/dist/test/anthropic.test.d.ts +1 -0
- package/dist/test/anthropic.test.js +793 -0
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +25 -0
- package/dist/test/boundary.test.d.ts +1 -0
- package/dist/test/boundary.test.js +32 -0
- package/dist/test/chat.test.d.ts +1 -0
- package/dist/test/chat.test.js +418 -0
- package/dist/test/cost.test.d.ts +1 -0
- package/dist/test/cost.test.js +60 -0
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +100 -0
- package/dist/test/drain.test.d.ts +1 -0
- package/dist/test/drain.test.js +116 -0
- package/dist/test/dropped.test.d.ts +1 -0
- package/dist/test/dropped.test.js +80 -0
- package/dist/test/endpoint-health.test.d.ts +1 -0
- package/dist/test/endpoint-health.test.js +73 -0
- package/dist/test/provenance.test.d.ts +1 -0
- package/dist/test/provenance.test.js +176 -0
- package/dist/test/provider-backends.test.d.ts +1 -0
- package/dist/test/provider-backends.test.js +699 -0
- package/dist/test/responses.test.d.ts +1 -0
- package/dist/test/responses.test.js +813 -0
- package/dist/test/routed-backend.test.d.ts +1 -0
- package/dist/test/routed-backend.test.js +39 -0
- package/dist/test/router.test.d.ts +1 -0
- package/dist/test/router.test.js +297 -0
- package/dist/test/server-resilience.test.d.ts +1 -0
- package/dist/test/server-resilience.test.js +169 -0
- package/dist/test/sse-codec.test.d.ts +1 -0
- package/dist/test/sse-codec.test.js +186 -0
- package/dist/test/web-search-loop.test.d.ts +1 -0
- package/dist/test/web-search-loop.test.js +469 -0
- package/dist/test/wire-validation.test.d.ts +1 -0
- package/dist/test/wire-validation.test.js +140 -0
- package/package.json +48 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Responses adapter. Codex speaks the Responses API exclusively
|
|
3
|
+
* (`wire_api="responses"`; Chat Completions support was removed), so to back it
|
|
4
|
+
* with a local model we translate `/v1/responses` to and from the gateway's
|
|
5
|
+
* OpenAI Chat Completions core. The pure translation functions are exported for
|
|
6
|
+
* testing; the handler returns a `Response` the server pipes (JSON or SSE).
|
|
7
|
+
*
|
|
8
|
+
* This is the highest-fidelity adapter: it maps Responses `input` items
|
|
9
|
+
* (messages, function calls, function-call outputs) into chat messages, and
|
|
10
|
+
* emits the Responses streaming event sequence (`response.created`,
|
|
11
|
+
* `response.output_item.added`, `response.output_text.delta`,
|
|
12
|
+
* `response.function_call_arguments.delta`, `response.completed`, …) from chat
|
|
13
|
+
* completion chunks.
|
|
14
|
+
*/
|
|
15
|
+
import { randomId } from "@velum-labs/routekit-runtime";
|
|
16
|
+
import { attachReasoningSelection, attachReasoningSelectionError } from "./openai-chat-wire.js";
|
|
17
|
+
import { droppedField } from "./dropped.js";
|
|
18
|
+
import { unwrapUpstreamError } from "./upstream-error.js";
|
|
19
|
+
import { openAiSseToResponses } from "./responses-stream.js";
|
|
20
|
+
import { composeServerToolStream, runBufferedServerToolLoop } from "./server-tool-loop.js";
|
|
21
|
+
import { resolveWebSearchExecutor } from "./web-search.js";
|
|
22
|
+
export { openAiSseToResponses } from "./responses-stream.js";
|
|
23
|
+
function partText(part) {
|
|
24
|
+
if (part.type === "refusal" && typeof part.text === "string")
|
|
25
|
+
return part.text;
|
|
26
|
+
if (typeof part.text === "string" && (part.type === "input_text" || part.type === "output_text" || part.type === "text")) {
|
|
27
|
+
return part.text;
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
function mapTextFormat(text) {
|
|
32
|
+
const format = text.format;
|
|
33
|
+
if (format === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
switch (format.type) {
|
|
36
|
+
case "json_schema":
|
|
37
|
+
return {
|
|
38
|
+
type: "json_schema",
|
|
39
|
+
json_schema: {
|
|
40
|
+
...(typeof format.name === "string" ? { name: format.name } : {}),
|
|
41
|
+
...(format.schema !== undefined ? { schema: format.schema } : {}),
|
|
42
|
+
...(typeof format.strict === "boolean" ? { strict: format.strict } : {})
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
case "json_object":
|
|
46
|
+
return { type: "json_object" };
|
|
47
|
+
default:
|
|
48
|
+
droppedField("responses", "text");
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function contentToText(content) {
|
|
53
|
+
if (typeof content === "string")
|
|
54
|
+
return content;
|
|
55
|
+
return content.map(partText).join("");
|
|
56
|
+
}
|
|
57
|
+
function contentToParts(content) {
|
|
58
|
+
if (typeof content === "string")
|
|
59
|
+
return content;
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (const part of content) {
|
|
62
|
+
if (part.type === "input_image" && typeof part.image_url === "string") {
|
|
63
|
+
parts.push({ type: "image_url", image_url: { url: part.image_url } });
|
|
64
|
+
}
|
|
65
|
+
else if (part.type === "input_file") {
|
|
66
|
+
droppedField("responses", "input_file");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const text = partText(part);
|
|
70
|
+
if (text.length > 0)
|
|
71
|
+
parts.push({ type: "text", text });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (parts.length === 1 && parts[0]?.type === "text") {
|
|
75
|
+
return String(parts[0].text);
|
|
76
|
+
}
|
|
77
|
+
return parts;
|
|
78
|
+
}
|
|
79
|
+
function mapToolChoice(choice) {
|
|
80
|
+
if (typeof choice === "string")
|
|
81
|
+
return choice;
|
|
82
|
+
// A typed tool choice (e.g. {type: "tool_search"}) resolves to the name the
|
|
83
|
+
// tool is projected under on the chat side (its type).
|
|
84
|
+
const name = choice.name ?? (choice.type !== "function" ? choice.type : undefined);
|
|
85
|
+
if (name === undefined || name.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
return { type: "function", function: { name } };
|
|
88
|
+
}
|
|
89
|
+
const EMPTY_TOOL_REGISTRY = new Map();
|
|
90
|
+
/**
|
|
91
|
+
* Whether a declared tool is a *typed, client-executed* tool: identified only
|
|
92
|
+
* by its `type` (no `name`) and executed by the caller (`execution: "client"`,
|
|
93
|
+
* e.g. Codex's `tool_search` for deferred-tool discovery). Server-executed
|
|
94
|
+
* typed tools (e.g. `web_search`, which OpenAI's backend runs) are excluded —
|
|
95
|
+
* the gateway cannot honor them, so advertising them to the upstream model would
|
|
96
|
+
* produce calls nobody executes.
|
|
97
|
+
*/
|
|
98
|
+
function isClientTypedTool(tool) {
|
|
99
|
+
return (typeof tool.type === "string" &&
|
|
100
|
+
tool.type.length > 0 &&
|
|
101
|
+
tool.type !== "function" &&
|
|
102
|
+
tool.type !== "custom" &&
|
|
103
|
+
(tool.name === undefined || tool.name.length === 0) &&
|
|
104
|
+
tool.execution === "client");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A declared server-executed web search tool (Codex's `{type: "web_search"}`,
|
|
108
|
+
* or variants like `web_search_preview`). When a web-search executor is
|
|
109
|
+
* available the gateway runs these itself (see `server-tool-loop.ts`);
|
|
110
|
+
* otherwise they are dropped with a warning as before.
|
|
111
|
+
*/
|
|
112
|
+
function isServerWebSearchTool(tool) {
|
|
113
|
+
return (typeof tool.type === "string" &&
|
|
114
|
+
tool.type.startsWith("web_search") &&
|
|
115
|
+
(tool.name === undefined || tool.name.length === 0) &&
|
|
116
|
+
tool.execution !== "client");
|
|
117
|
+
}
|
|
118
|
+
/** The name the gateway-executed web search tool is projected under chat-side. */
|
|
119
|
+
export const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
120
|
+
const WEB_SEARCH_TOOL_DESCRIPTION = "Search the web for current, factual information. The search runs server-side and " +
|
|
121
|
+
"returns result text with source URLs. Use it when the answer depends on information " +
|
|
122
|
+
"that may have changed since your training data.";
|
|
123
|
+
const WEB_SEARCH_TOOL_PARAMETERS = {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
query: { type: "string", description: "The web search query." }
|
|
127
|
+
},
|
|
128
|
+
required: ["query"],
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
};
|
|
131
|
+
function collectDiscovered(entries, namespace, out) {
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (typeof entry !== "object" || entry === null)
|
|
134
|
+
continue;
|
|
135
|
+
const record = entry;
|
|
136
|
+
if (record.type === "namespace" && Array.isArray(record.tools)) {
|
|
137
|
+
collectDiscovered(record.tools, typeof record.name === "string" ? record.name : namespace, out);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (typeof record.name === "string" && record.name.length > 0) {
|
|
141
|
+
out.push({
|
|
142
|
+
name: record.name,
|
|
143
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
144
|
+
...(typeof record.description === "string" ? { description: record.description } : {}),
|
|
145
|
+
...(record.parameters !== undefined ? { parameters: record.parameters } : {})
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The tools *discovered* through prior typed-tool executions in this
|
|
152
|
+
* conversation. Codex never adds discovered tools to the request's `tools`
|
|
153
|
+
* array — their definitions ride inside the echoed `tool_search_output` items
|
|
154
|
+
* (possibly grouped under namespaces) and Codex dispatches subsequent calls by
|
|
155
|
+
* name + namespace. Harvesting them here is what closes the discovery loop:
|
|
156
|
+
* the follow-up model turn can advertise and call `spawn_agent` etc.
|
|
157
|
+
*/
|
|
158
|
+
function discoveredToolsFromInput(body) {
|
|
159
|
+
const found = [];
|
|
160
|
+
if (!Array.isArray(body.input))
|
|
161
|
+
return found;
|
|
162
|
+
for (const item of body.input) {
|
|
163
|
+
if (typeof item !== "object" || item === null)
|
|
164
|
+
continue;
|
|
165
|
+
const record = item;
|
|
166
|
+
if (typeof record.type !== "string" || !record.type.endsWith("_output"))
|
|
167
|
+
continue;
|
|
168
|
+
if (!Array.isArray(record.tools))
|
|
169
|
+
continue;
|
|
170
|
+
collectDiscovered(record.tools, undefined, found);
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The per-request tool registry: every callable tool the request declares or
|
|
176
|
+
* has discovered, keyed by the name the chat-side model calls it under, mapped
|
|
177
|
+
* to how its calls must be emitted. Typed tools are keyed by their `type`;
|
|
178
|
+
* discovered tools carry their namespace for egress dispatch.
|
|
179
|
+
*/
|
|
180
|
+
export function responsesToolRegistry(body, options = {}) {
|
|
181
|
+
const registry = new Map();
|
|
182
|
+
for (const tool of body.tools ?? []) {
|
|
183
|
+
if (typeof tool.name === "string" && tool.name.length > 0) {
|
|
184
|
+
registry.set(tool.name, { kind: tool.type === "custom" ? "custom" : "function" });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (isClientTypedTool(tool))
|
|
188
|
+
registry.set(tool.type, { kind: "typed" });
|
|
189
|
+
else if (options.serverTools === true && isServerWebSearchTool(tool)) {
|
|
190
|
+
registry.set(WEB_SEARCH_TOOL_NAME, { kind: "server" });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const tool of discoveredToolsFromInput(body)) {
|
|
194
|
+
if (registry.has(tool.name))
|
|
195
|
+
continue;
|
|
196
|
+
registry.set(tool.name, {
|
|
197
|
+
kind: "function",
|
|
198
|
+
...(tool.namespace !== undefined ? { namespace: tool.namespace } : {})
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return registry;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Back-compat helper: the names of the freeform ("custom") tools a Responses
|
|
205
|
+
* request declares (see {@link responsesToolRegistry}).
|
|
206
|
+
*/
|
|
207
|
+
export function customToolNames(body) {
|
|
208
|
+
const names = new Set();
|
|
209
|
+
for (const [name, entry] of responsesToolRegistry(body)) {
|
|
210
|
+
if (entry.kind === "custom")
|
|
211
|
+
names.add(name);
|
|
212
|
+
}
|
|
213
|
+
return names;
|
|
214
|
+
}
|
|
215
|
+
/** The chat function-tool schema a custom tool is forwarded as. */
|
|
216
|
+
const CUSTOM_TOOL_PARAMETERS = {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
input: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "The complete raw text input for this tool (not JSON-encoded)."
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
required: ["input"],
|
|
225
|
+
additionalProperties: false
|
|
226
|
+
};
|
|
227
|
+
/** Fold a custom tool's freeform contract (and grammar, if any) into a
|
|
228
|
+
* description the chat-side model can actually follow. */
|
|
229
|
+
function customToolDescription(tool) {
|
|
230
|
+
const parts = [];
|
|
231
|
+
if (typeof tool.description === "string" && tool.description.length > 0)
|
|
232
|
+
parts.push(tool.description);
|
|
233
|
+
parts.push(`This is a freeform tool: put the ENTIRE raw tool input as one string in the "input" field. ` +
|
|
234
|
+
`Do not wrap it in any other JSON structure.`);
|
|
235
|
+
const definition = tool.format?.definition;
|
|
236
|
+
if (typeof definition === "string" && definition.length > 0) {
|
|
237
|
+
const syntax = tool.format?.syntax;
|
|
238
|
+
parts.push(`The input must conform to this ${syntax ?? "grammar"}:\n${definition}`);
|
|
239
|
+
}
|
|
240
|
+
return parts.join("\n\n");
|
|
241
|
+
}
|
|
242
|
+
/** Extract the raw string input from a chat tool call's accumulated arguments.
|
|
243
|
+
* The model was asked for `{"input": "..."}`; a model that emitted the raw
|
|
244
|
+
* text directly (non-JSON) is passed through verbatim. */
|
|
245
|
+
function customToolInput(args) {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(args);
|
|
248
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.input === "string") {
|
|
249
|
+
return parsed.input;
|
|
250
|
+
}
|
|
251
|
+
if (typeof parsed === "string")
|
|
252
|
+
return parsed;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// not JSON: treat the whole argument string as the raw input
|
|
256
|
+
}
|
|
257
|
+
return args;
|
|
258
|
+
}
|
|
259
|
+
/** Translate a Responses request to an OpenAI Chat Completions body. */
|
|
260
|
+
export function responsesToChat(body, backendModel, options = {}) {
|
|
261
|
+
const messages = [];
|
|
262
|
+
if (typeof body.instructions === "string" && body.instructions.length > 0) {
|
|
263
|
+
messages.push({ role: "system", content: body.instructions });
|
|
264
|
+
}
|
|
265
|
+
const input = body.input;
|
|
266
|
+
if (typeof input === "string") {
|
|
267
|
+
messages.push({ role: "user", content: input });
|
|
268
|
+
}
|
|
269
|
+
else if (Array.isArray(input)) {
|
|
270
|
+
// Coalesce consecutive function_call items into ONE assistant message.
|
|
271
|
+
// Codex emits parallel tool calls as separate function_call items; the chat
|
|
272
|
+
// API requires an assistant message's tool_calls to be answered by the
|
|
273
|
+
// following tool messages before the next assistant message, so each call
|
|
274
|
+
// must not become its own assistant turn.
|
|
275
|
+
let pendingToolCalls = [];
|
|
276
|
+
// The assistant text message immediately preceding the pending function
|
|
277
|
+
// calls, if any. A model that answered with text + tool calls in one turn
|
|
278
|
+
// is replayed by Codex as a message item followed by function_call items;
|
|
279
|
+
// they must fold back into ONE assistant message (content + tool_calls).
|
|
280
|
+
// Split across two assistant messages, some models (qwen3-coder) read the
|
|
281
|
+
// text-only turn as a completed turn and stop calling tools entirely.
|
|
282
|
+
let pendingAssistantText;
|
|
283
|
+
const flushToolCalls = () => {
|
|
284
|
+
if (pendingToolCalls.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
if (pendingAssistantText !== undefined) {
|
|
287
|
+
pendingAssistantText.tool_calls = pendingToolCalls;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
messages.push({ role: "assistant", content: null, tool_calls: pendingToolCalls });
|
|
291
|
+
}
|
|
292
|
+
pendingToolCalls = [];
|
|
293
|
+
pendingAssistantText = undefined;
|
|
294
|
+
};
|
|
295
|
+
for (const item of input) {
|
|
296
|
+
if (item.type === "function_call") {
|
|
297
|
+
const call = item;
|
|
298
|
+
pendingToolCalls.push({
|
|
299
|
+
id: call.call_id ?? call.id ?? `call_${randomId()}`,
|
|
300
|
+
type: "function",
|
|
301
|
+
function: { name: call.name, arguments: call.arguments }
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
// A prior custom (freeform) tool call echoed back by the caller. Re-encode
|
|
306
|
+
// its raw input as the `{input}` JSON arguments the chat side uses, so the
|
|
307
|
+
// conversation round-trips losslessly.
|
|
308
|
+
if (item.type === "custom_tool_call") {
|
|
309
|
+
const call = item;
|
|
310
|
+
pendingToolCalls.push({
|
|
311
|
+
id: call.call_id ?? call.id ?? `call_${randomId()}`,
|
|
312
|
+
type: "function",
|
|
313
|
+
function: { name: call.name, arguments: JSON.stringify({ input: call.input ?? "" }) }
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// A prior gateway-executed web search echoed back. Codex echoes these
|
|
318
|
+
// items with no id/call_id and no results (on the real API results live
|
|
319
|
+
// server-side), so the exchange cannot round-trip as a chat tool call —
|
|
320
|
+
// fold it into the transcript as assistant context so the upstream model
|
|
321
|
+
// remembers the search happened and does not blindly repeat it.
|
|
322
|
+
if (item.type === "web_search_call") {
|
|
323
|
+
flushToolCalls();
|
|
324
|
+
pendingAssistantText = undefined;
|
|
325
|
+
const action = item.action;
|
|
326
|
+
const query = typeof action?.query === "string" ? action.query : "";
|
|
327
|
+
messages.push({
|
|
328
|
+
role: "assistant",
|
|
329
|
+
content: query.length > 0 ? `[searched the web for: ${JSON.stringify(query)}]` : "[searched the web]"
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// A prior *typed* tool call echoed back (e.g. `tool_search_call`): replay
|
|
334
|
+
// it as an assistant tool call under the tool's projected name (its item
|
|
335
|
+
// type minus `_call`) so the chat-side history stays coherent across the
|
|
336
|
+
// caller's discovery/execution loop.
|
|
337
|
+
if (typeof item.type === "string" &&
|
|
338
|
+
item.type.endsWith("_call") &&
|
|
339
|
+
item.type !== "function_call" &&
|
|
340
|
+
item.type !== "custom_tool_call" &&
|
|
341
|
+
typeof item.call_id === "string") {
|
|
342
|
+
const call = item;
|
|
343
|
+
pendingToolCalls.push({
|
|
344
|
+
id: call.call_id,
|
|
345
|
+
type: "function",
|
|
346
|
+
function: {
|
|
347
|
+
name: call.type.slice(0, -"_call".length),
|
|
348
|
+
arguments: typeof call.arguments === "string"
|
|
349
|
+
? call.arguments
|
|
350
|
+
: JSON.stringify(call.arguments ?? {})
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
flushToolCalls();
|
|
356
|
+
// Reasoning items round-trip: Codex echoes the reasoning item back
|
|
357
|
+
// verbatim on the next request (with `summary`, and `content` that may be
|
|
358
|
+
// null). Drop it — narration must never leak into the provider prompt
|
|
359
|
+
// (mirrors the Anthropic adapter dropping thinking blocks). A reasoning
|
|
360
|
+
// item may sit between an assistant message and its function calls, so
|
|
361
|
+
// dropping it must not break their adjacency (`pendingAssistantText`
|
|
362
|
+
// survives; every other item type invalidates it below).
|
|
363
|
+
if (item.type === "reasoning") {
|
|
364
|
+
droppedField("responses", "reasoning", "input");
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
pendingAssistantText = undefined;
|
|
368
|
+
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
369
|
+
const out = item;
|
|
370
|
+
const content = typeof out.output === "string" ? out.output : JSON.stringify(out.output);
|
|
371
|
+
messages.push({ role: "tool", tool_call_id: out.call_id, content });
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// A typed tool's result (e.g. `tool_search_output`): the whole item body
|
|
375
|
+
// (minus wire boilerplate) is the tool result the chat side reads — for
|
|
376
|
+
// tool_search that is the discovered tool list.
|
|
377
|
+
if (typeof item.type === "string" &&
|
|
378
|
+
item.type.endsWith("_output") &&
|
|
379
|
+
typeof item.call_id === "string") {
|
|
380
|
+
const { type: _type, call_id, id: _id, ...rest } = item;
|
|
381
|
+
messages.push({ role: "tool", tool_call_id: call_id, content: JSON.stringify(rest) });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
// message item (explicit type "message" or a bare {role, content}); any
|
|
385
|
+
// other item type without string/array content is skipped, never iterated.
|
|
386
|
+
const message = item;
|
|
387
|
+
if (typeof message.content !== "string" && !Array.isArray(message.content))
|
|
388
|
+
continue;
|
|
389
|
+
const role = message.role === "developer" ? "system" : message.role ?? "user";
|
|
390
|
+
const chatMessage = { role, content: contentToParts(message.content) };
|
|
391
|
+
messages.push(chatMessage);
|
|
392
|
+
if (role === "assistant")
|
|
393
|
+
pendingAssistantText = chatMessage;
|
|
394
|
+
}
|
|
395
|
+
flushToolCalls();
|
|
396
|
+
}
|
|
397
|
+
const chat = {
|
|
398
|
+
model: backendModel ?? body.model ?? "",
|
|
399
|
+
messages,
|
|
400
|
+
stream: body.stream === true
|
|
401
|
+
};
|
|
402
|
+
if (typeof body.max_output_tokens === "number")
|
|
403
|
+
chat.max_completion_tokens = body.max_output_tokens;
|
|
404
|
+
if (typeof body.temperature === "number")
|
|
405
|
+
chat.temperature = body.temperature;
|
|
406
|
+
if (typeof body.top_p === "number")
|
|
407
|
+
chat.top_p = body.top_p;
|
|
408
|
+
if (typeof body.parallel_tool_calls === "boolean")
|
|
409
|
+
chat.parallel_tool_calls = body.parallel_tool_calls;
|
|
410
|
+
// `reasoning: null` means "this model has no reasoning config" (Codex sends
|
|
411
|
+
// it for every custom provider model slug) — skip silently rather
|
|
412
|
+
// than treating it as an untranslatable field, and never dereference it.
|
|
413
|
+
if (body.reasoning != null) {
|
|
414
|
+
const effort = body.reasoning.effort;
|
|
415
|
+
if (effort == null) {
|
|
416
|
+
// Current Codex releases send `{ effort: null }` when no reasoning
|
|
417
|
+
// control was selected. Treat it exactly like `reasoning: null`.
|
|
418
|
+
}
|
|
419
|
+
else if (typeof effort === "string" && effort.length > 0) {
|
|
420
|
+
chat.reasoning_effort = effort;
|
|
421
|
+
attachReasoningSelection(chat, { mode: "effort", effort });
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
attachReasoningSelectionError(chat, "reasoning.effort must be a non-empty string");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (body.text != null) {
|
|
428
|
+
const responseFormat = mapTextFormat(body.text);
|
|
429
|
+
if (responseFormat !== undefined)
|
|
430
|
+
chat.response_format = responseFormat;
|
|
431
|
+
}
|
|
432
|
+
if (body.previous_response_id != null)
|
|
433
|
+
droppedField("responses", "previous_response_id");
|
|
434
|
+
if (body.truncation != null)
|
|
435
|
+
droppedField("responses", "truncation");
|
|
436
|
+
if (body.metadata != null)
|
|
437
|
+
droppedField("responses", "metadata");
|
|
438
|
+
if (body.include != null && body.include.length > 0)
|
|
439
|
+
droppedField("responses", "include");
|
|
440
|
+
{
|
|
441
|
+
// Every callable tool is forwarded as a chat function tool (Chat
|
|
442
|
+
// Completions only speaks JSON function tools):
|
|
443
|
+
// - named function tools pass through;
|
|
444
|
+
// - a freeform "custom" tool (e.g. Codex's `apply_patch`) has no JSON
|
|
445
|
+
// schema — it becomes a function tool with an `{input: string}` schema
|
|
446
|
+
// and its grammar folded into the description;
|
|
447
|
+
// - a typed client-executed tool (e.g. Codex's `tool_search`, the
|
|
448
|
+
// deferred-tool discovery door) is projected under its `type` as the
|
|
449
|
+
// function name, so the chat-side model can call it and the caller can
|
|
450
|
+
// dispatch it (the egress emits its native `<type>_call` item).
|
|
451
|
+
// Server-executed typed tools (e.g. `web_search`) are excluded: nothing on
|
|
452
|
+
// this side can run them, so advertising them would only produce calls
|
|
453
|
+
// nobody answers.
|
|
454
|
+
const tools = [];
|
|
455
|
+
for (const tool of body.tools ?? []) {
|
|
456
|
+
if (typeof tool.name === "string" && tool.name.length > 0) {
|
|
457
|
+
tools.push(tool.type === "custom"
|
|
458
|
+
? {
|
|
459
|
+
type: "function",
|
|
460
|
+
function: {
|
|
461
|
+
name: tool.name,
|
|
462
|
+
description: customToolDescription(tool),
|
|
463
|
+
parameters: CUSTOM_TOOL_PARAMETERS
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
: {
|
|
467
|
+
type: "function",
|
|
468
|
+
function: {
|
|
469
|
+
name: tool.name,
|
|
470
|
+
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
471
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (isClientTypedTool(tool)) {
|
|
477
|
+
tools.push({
|
|
478
|
+
type: "function",
|
|
479
|
+
function: {
|
|
480
|
+
name: tool.type,
|
|
481
|
+
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
482
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
else if (options.serverTools === true && isServerWebSearchTool(tool)) {
|
|
487
|
+
// Server-executed web search, honored by the gateway's server-tool
|
|
488
|
+
// loop: projected as an ordinary function tool the upstream model can
|
|
489
|
+
// call; the loop intercepts and executes the calls.
|
|
490
|
+
if (!tools.some((t) => t.function?.name === WEB_SEARCH_TOOL_NAME)) {
|
|
491
|
+
tools.push({
|
|
492
|
+
type: "function",
|
|
493
|
+
function: {
|
|
494
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
495
|
+
description: WEB_SEARCH_TOOL_DESCRIPTION,
|
|
496
|
+
parameters: WEB_SEARCH_TOOL_PARAMETERS
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (typeof tool.type === "string" &&
|
|
502
|
+
tool.type.length > 0 &&
|
|
503
|
+
tool.type !== "function" &&
|
|
504
|
+
tool.type !== "custom" &&
|
|
505
|
+
(tool.name === undefined || tool.name.length === 0)) {
|
|
506
|
+
droppedField("responses", tool.type, "tools");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Tools discovered mid-conversation (via a prior `tool_search` execution)
|
|
510
|
+
// never appear in the request's `tools` array — the caller expects them to
|
|
511
|
+
// be callable anyway, so they must be advertised to the chat-side model.
|
|
512
|
+
const declared = new Set(tools.map((tool) => tool.function?.name));
|
|
513
|
+
for (const tool of discoveredToolsFromInput(body)) {
|
|
514
|
+
if (declared.has(tool.name))
|
|
515
|
+
continue;
|
|
516
|
+
declared.add(tool.name);
|
|
517
|
+
tools.push({
|
|
518
|
+
type: "function",
|
|
519
|
+
function: {
|
|
520
|
+
name: tool.name,
|
|
521
|
+
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
522
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (tools.length > 0)
|
|
527
|
+
chat.tools = tools;
|
|
528
|
+
}
|
|
529
|
+
if (body.tool_choice != null) {
|
|
530
|
+
const choice = mapToolChoice(body.tool_choice);
|
|
531
|
+
if (choice !== undefined)
|
|
532
|
+
chat.tool_choice = choice;
|
|
533
|
+
}
|
|
534
|
+
if (body.stream === true)
|
|
535
|
+
chat.stream_options = { include_usage: true };
|
|
536
|
+
return chat;
|
|
537
|
+
}
|
|
538
|
+
// ---- non-streaming response translation ----
|
|
539
|
+
/** Parse a typed tool call's accumulated arguments into the JSON value its
|
|
540
|
+
* native item carries (`arguments` is an object on the wire, not a string). */
|
|
541
|
+
function typedToolArguments(args) {
|
|
542
|
+
if (args.trim().length === 0)
|
|
543
|
+
return {};
|
|
544
|
+
try {
|
|
545
|
+
return JSON.parse(args);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return {};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** The native item for a typed tool call (e.g. name "tool_search" ->
|
|
552
|
+
* `tool_search_call`). Codex dispatches typed tools by payload shape; a
|
|
553
|
+
* `function_call` under the same name fails with "unsupported payload". */
|
|
554
|
+
function typedToolCallItem(input) {
|
|
555
|
+
return {
|
|
556
|
+
type: `${input.name}_call`,
|
|
557
|
+
id: input.itemId,
|
|
558
|
+
call_id: input.callId,
|
|
559
|
+
status: "completed",
|
|
560
|
+
execution: "client",
|
|
561
|
+
arguments: typedToolArguments(input.args)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/** The `query` from a web_search call's JSON arguments (raw args as fallback). */
|
|
565
|
+
function webSearchQueryOf(args) {
|
|
566
|
+
try {
|
|
567
|
+
const parsed = JSON.parse(args);
|
|
568
|
+
if (typeof parsed.query === "string")
|
|
569
|
+
return parsed.query;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// fall through to the raw argument string
|
|
573
|
+
}
|
|
574
|
+
return args;
|
|
575
|
+
}
|
|
576
|
+
/** The native output item for a gateway-executed web search. */
|
|
577
|
+
function executedSearchItem(search) {
|
|
578
|
+
return {
|
|
579
|
+
type: "web_search_call",
|
|
580
|
+
id: search.itemId,
|
|
581
|
+
status: search.status === "completed" ? "completed" : "failed",
|
|
582
|
+
action: { type: "search", query: search.query }
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function buildOutput(message, toolRegistry) {
|
|
586
|
+
const output = [];
|
|
587
|
+
const reasoning = typeof message?.reasoning === "string" && message.reasoning.length > 0
|
|
588
|
+
? message.reasoning
|
|
589
|
+
: typeof message?.reasoning_content === "string" && message.reasoning_content.length > 0
|
|
590
|
+
? message.reasoning_content
|
|
591
|
+
: "";
|
|
592
|
+
if (reasoning.length > 0) {
|
|
593
|
+
// A reasoning-only turn must still produce output: without this item an
|
|
594
|
+
// all-thinking response assembles as `output: []`, which callers (codex)
|
|
595
|
+
// treat as an empty turn and retry.
|
|
596
|
+
output.push({
|
|
597
|
+
type: "reasoning",
|
|
598
|
+
id: `rs_${randomId()}`,
|
|
599
|
+
summary: [{ type: "summary_text", text: reasoning }]
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const text = typeof message?.content === "string" ? message.content : "";
|
|
603
|
+
if (text.length > 0) {
|
|
604
|
+
output.push({
|
|
605
|
+
type: "message",
|
|
606
|
+
id: `msg_${randomId()}`,
|
|
607
|
+
status: "completed",
|
|
608
|
+
role: "assistant",
|
|
609
|
+
content: [{ type: "output_text", text, annotations: [] }]
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (Array.isArray(message?.tool_calls)) {
|
|
613
|
+
for (const call of message.tool_calls) {
|
|
614
|
+
const name = call.function?.name ?? "";
|
|
615
|
+
const args = call.function?.arguments ?? "";
|
|
616
|
+
const entry = toolRegistry.get(name) ?? { kind: "function" };
|
|
617
|
+
if (entry.kind === "custom") {
|
|
618
|
+
// The caller declared this tool as freeform: it expects a
|
|
619
|
+
// `custom_tool_call` item carrying the raw string input.
|
|
620
|
+
output.push({
|
|
621
|
+
type: "custom_tool_call",
|
|
622
|
+
id: `ctc_${randomId()}`,
|
|
623
|
+
call_id: call.id ?? `call_${randomId()}`,
|
|
624
|
+
name,
|
|
625
|
+
input: customToolInput(args),
|
|
626
|
+
status: "completed"
|
|
627
|
+
});
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (entry.kind === "typed") {
|
|
631
|
+
output.push(typedToolCallItem({
|
|
632
|
+
name,
|
|
633
|
+
itemId: `ttc_${randomId()}`,
|
|
634
|
+
callId: call.id ?? `call_${randomId()}`,
|
|
635
|
+
args
|
|
636
|
+
}));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (entry.kind === "server") {
|
|
640
|
+
// Unreachable in practice: the server-tool loop intercepts these calls
|
|
641
|
+
// before they reach a terminal message. Render the native item shape
|
|
642
|
+
// (never a function_call — nobody on the caller's side dispatches it).
|
|
643
|
+
output.push({
|
|
644
|
+
type: "web_search_call",
|
|
645
|
+
id: `ws_${randomId()}`,
|
|
646
|
+
status: "completed",
|
|
647
|
+
action: { type: "search", query: webSearchQueryOf(args) }
|
|
648
|
+
});
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
output.push({
|
|
652
|
+
type: "function_call",
|
|
653
|
+
id: `fc_${randomId()}`,
|
|
654
|
+
call_id: call.id ?? `call_${randomId()}`,
|
|
655
|
+
name,
|
|
656
|
+
// A discovered tool's call routes by name *and* namespace.
|
|
657
|
+
...(entry.namespace !== undefined ? { namespace: entry.namespace } : {}),
|
|
658
|
+
arguments: args,
|
|
659
|
+
status: "completed"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return output;
|
|
664
|
+
}
|
|
665
|
+
export function chatToResponses(openai, model, toolRegistry = EMPTY_TOOL_REGISTRY, searches = []) {
|
|
666
|
+
const message = openai.choices?.[0]?.message;
|
|
667
|
+
// Gateway-executed searches happened before the terminal step's output.
|
|
668
|
+
const output = [...searches.map(executedSearchItem), ...buildOutput(message, toolRegistry)];
|
|
669
|
+
const inputTokens = openai.usage?.prompt_tokens;
|
|
670
|
+
const outputTokens = openai.usage?.completion_tokens;
|
|
671
|
+
return {
|
|
672
|
+
id: `resp_${openai.id ?? randomId()}`,
|
|
673
|
+
object: "response",
|
|
674
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
675
|
+
status: "completed",
|
|
676
|
+
model,
|
|
677
|
+
output,
|
|
678
|
+
usage: inputTokens !== undefined || outputTokens !== undefined
|
|
679
|
+
? {
|
|
680
|
+
...(inputTokens !== undefined ? { input_tokens: inputTokens } : {}),
|
|
681
|
+
...(outputTokens !== undefined ? { output_tokens: outputTokens } : {}),
|
|
682
|
+
...(inputTokens !== undefined && outputTokens !== undefined
|
|
683
|
+
? { total_tokens: inputTokens + outputTokens }
|
|
684
|
+
: {})
|
|
685
|
+
}
|
|
686
|
+
: null,
|
|
687
|
+
...(openai.provider_cost !== undefined ? { provider_cost: openai.provider_cost } : {})
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// ---- streaming translation (OpenAI chat SSE -> Responses SSE) ----
|
|
691
|
+
// ---- streaming translation (OpenAI chat SSE -> Responses SSE) ----
|
|
692
|
+
// ---- handler ----
|
|
693
|
+
function jsonResponse(status, value) {
|
|
694
|
+
return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
|
695
|
+
}
|
|
696
|
+
export async function handleResponses(backend, body, modelCallId, signal, backendOptions = {}) {
|
|
697
|
+
const requestedModel = body.model ?? backend.defaultModel ?? "";
|
|
698
|
+
const resolvedModel = backend.resolveModel?.(body.model);
|
|
699
|
+
if (body.model !== undefined &&
|
|
700
|
+
backend.resolveModel !== undefined &&
|
|
701
|
+
resolvedModel === undefined) {
|
|
702
|
+
return jsonResponse(400, {
|
|
703
|
+
error: {
|
|
704
|
+
type: "invalid_request_error",
|
|
705
|
+
code: "model_not_found",
|
|
706
|
+
param: "model",
|
|
707
|
+
message: `unknown model: ${body.model}`
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const upstreamModel = resolvedModel ?? backend.defaultModel;
|
|
712
|
+
if (upstreamModel === undefined) {
|
|
713
|
+
return jsonResponse(503, {
|
|
714
|
+
error: {
|
|
715
|
+
type: "unavailable",
|
|
716
|
+
message: "no model is available; configure a provider"
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
// Server-executed web search is honored when the caller declared the tool,
|
|
721
|
+
// an executor is available (a provider key exists), and no *client* tool
|
|
722
|
+
// already owns the projected name; otherwise the ingress keeps its
|
|
723
|
+
// honest-drop behavior.
|
|
724
|
+
const declaresWebSearch = body.tools?.some(isServerWebSearchTool) === true;
|
|
725
|
+
const clientNameCollision = body.tools?.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME) === true;
|
|
726
|
+
const executor = declaresWebSearch && !clientNameCollision ? resolveWebSearchExecutor("responses") : undefined;
|
|
727
|
+
const serverTools = executor !== undefined;
|
|
728
|
+
const toolRegistry = responsesToolRegistry(body, { serverTools });
|
|
729
|
+
const chat = responsesToChat(body, upstreamModel, { serverTools });
|
|
730
|
+
const requestOptions = {
|
|
731
|
+
...backendOptions,
|
|
732
|
+
modelCallId,
|
|
733
|
+
// The streamed response is translated to Responses SSE by
|
|
734
|
+
// openAiSseToResponses, which emits its own keepalive.
|
|
735
|
+
...(body.stream === true ? { translated: true } : {})
|
|
736
|
+
};
|
|
737
|
+
const upstream = await backend.chat(chat, signal, requestOptions);
|
|
738
|
+
if (!upstream.ok) {
|
|
739
|
+
const detail = await upstream.text();
|
|
740
|
+
return jsonResponse(upstream.status, { error: unwrapUpstreamError(detail) });
|
|
741
|
+
}
|
|
742
|
+
if (executor !== undefined) {
|
|
743
|
+
const loopOptions = {
|
|
744
|
+
chat,
|
|
745
|
+
runStep: (stepChat) => backend.chat(stepChat, signal, requestOptions),
|
|
746
|
+
serverToolNames: new Set([WEB_SEARCH_TOOL_NAME]),
|
|
747
|
+
executor,
|
|
748
|
+
...(signal !== undefined ? { signal } : {})
|
|
749
|
+
};
|
|
750
|
+
if (body.stream === true) {
|
|
751
|
+
const source = upstream.body;
|
|
752
|
+
if (source === null)
|
|
753
|
+
return jsonResponse(502, { error: { type: "api_error", message: "no upstream stream" } });
|
|
754
|
+
const composed = composeServerToolStream({ ...loopOptions, firstStep: upstream });
|
|
755
|
+
return new Response(openAiSseToResponses(composed, requestedModel, toolRegistry), {
|
|
756
|
+
status: 200,
|
|
757
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const outcome = await runBufferedServerToolLoop({ ...loopOptions, firstStep: upstream });
|
|
761
|
+
if (outcome.kind === "upstream_error") {
|
|
762
|
+
const detail = await outcome.response.text();
|
|
763
|
+
return jsonResponse(outcome.response.status, { error: { type: "api_error", message: detail.slice(0, 2000) } });
|
|
764
|
+
}
|
|
765
|
+
return jsonResponse(200, chatToResponses(outcome.openai, requestedModel, toolRegistry, outcome.searches));
|
|
766
|
+
}
|
|
767
|
+
if (body.stream === true) {
|
|
768
|
+
const source = upstream.body;
|
|
769
|
+
if (source === null)
|
|
770
|
+
return jsonResponse(502, { error: { type: "api_error", message: "no upstream stream" } });
|
|
771
|
+
return new Response(openAiSseToResponses(source, requestedModel, toolRegistry), {
|
|
772
|
+
status: 200,
|
|
773
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
const openai = (await upstream.json());
|
|
777
|
+
return jsonResponse(200, chatToResponses(openai, requestedModel, toolRegistry));
|
|
778
|
+
}
|