@dbx-tools/appkit-model-gateway 0.9.36
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/README.md +63 -0
- package/index.ts +28 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +21 -0
- package/lib/scripts/copy-manifest.d.ts +1 -0
- package/lib/scripts/copy-manifest.js +14 -0
- package/lib/src/gateway.d.ts +29 -0
- package/lib/src/gateway.js +127 -0
- package/lib/src/manifest.json +13 -0
- package/lib/src/models.d.ts +10 -0
- package/lib/src/models.js +119 -0
- package/lib/src/plugin.d.ts +32 -0
- package/lib/src/plugin.js +70 -0
- package/lib/src/protocols/decode.d.ts +17 -0
- package/lib/src/protocols/decode.js +262 -0
- package/lib/src/protocols/encode.d.ts +18 -0
- package/lib/src/protocols/encode.js +498 -0
- package/lib/src/reasoning-adapt.d.ts +27 -0
- package/lib/src/reasoning-adapt.js +61 -0
- package/lib/src/registry.d.ts +32 -0
- package/lib/src/registry.js +167 -0
- package/lib/src/router.d.ts +18 -0
- package/lib/src/router.js +139 -0
- package/lib/src/routes.d.ts +14 -0
- package/lib/src/routes.js +158 -0
- package/lib/src/translation.d.ts +8 -0
- package/lib/src/translation.js +156 -0
- package/lib/src/transport.d.ts +19 -0
- package/lib/src/transport.js +168 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +150 -0
- package/src/gateway.ts +167 -0
- package/src/manifest.json +13 -0
- package/src/models.ts +152 -0
- package/src/plugin.ts +95 -0
- package/src/protocols/decode.ts +281 -0
- package/src/protocols/encode.ts +515 -0
- package/src/reasoning-adapt.ts +87 -0
- package/src/registry.ts +225 -0
- package/src/router.ts +174 -0
- package/src/routes.ts +181 -0
- package/src/translation.ts +178 -0
- package/src/transport.ts +182 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming and buffered protocol encoders for AI SDK fallback results.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ClientProtocol } from "@dbx-tools/shared-model-gateway";
|
|
8
|
+
import type { FinishReason, LanguageModelUsage, TextStreamPart, ToolSet, TypedToolCall } from "ai";
|
|
9
|
+
|
|
10
|
+
/** Completed AI SDK generation data used by non-streaming encoders. */
|
|
11
|
+
export interface CompletedGeneration {
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly toolCalls: readonly TypedToolCall<ToolSet>[];
|
|
14
|
+
readonly usage: LanguageModelUsage;
|
|
15
|
+
readonly finishReason: FinishReason;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Encode AI SDK events as the requested protocol while preserving backpressure. */
|
|
19
|
+
export function encodeGatewayStream(
|
|
20
|
+
protocol: ClientProtocol,
|
|
21
|
+
model: string,
|
|
22
|
+
source: AsyncIterable<TextStreamPart<ToolSet>>,
|
|
23
|
+
): ReadableStream<Uint8Array> {
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
return new ReadableStream<Uint8Array>({
|
|
26
|
+
async start(controller) {
|
|
27
|
+
try {
|
|
28
|
+
const events =
|
|
29
|
+
protocol === "openai-responses"
|
|
30
|
+
? responsesEvents(model, source)
|
|
31
|
+
: protocol === "anthropic-messages"
|
|
32
|
+
? anthropicEvents(model, source)
|
|
33
|
+
: chatEvents(model, source);
|
|
34
|
+
for await (const event of events) controller.enqueue(encoder.encode(event));
|
|
35
|
+
controller.close();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
controller.error(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Encode a completed AI SDK generation as one protocol-native JSON response. */
|
|
44
|
+
export function encodeGatewayResponse(
|
|
45
|
+
protocol: ClientProtocol,
|
|
46
|
+
model: string,
|
|
47
|
+
generation: CompletedGeneration,
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
if (protocol === "openai-responses") return responsesObject(model, generation);
|
|
50
|
+
if (protocol === "anthropic-messages") return anthropicObject(model, generation);
|
|
51
|
+
return chatObject(model, generation);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function* responsesEvents(
|
|
55
|
+
model: string,
|
|
56
|
+
source: AsyncIterable<TextStreamPart<ToolSet>>,
|
|
57
|
+
): AsyncGenerator<string> {
|
|
58
|
+
const responseId = `resp_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
59
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
60
|
+
const output: Record<string, unknown>[] = [];
|
|
61
|
+
const response = {
|
|
62
|
+
id: responseId,
|
|
63
|
+
object: "response",
|
|
64
|
+
created_at: createdAt,
|
|
65
|
+
model,
|
|
66
|
+
status: "in_progress",
|
|
67
|
+
background: false,
|
|
68
|
+
completed_at: null,
|
|
69
|
+
error: null,
|
|
70
|
+
incomplete_details: null,
|
|
71
|
+
instructions: null,
|
|
72
|
+
max_output_tokens: null,
|
|
73
|
+
output,
|
|
74
|
+
parallel_tool_calls: true,
|
|
75
|
+
previous_response_id: null,
|
|
76
|
+
reasoning: null,
|
|
77
|
+
store: false,
|
|
78
|
+
temperature: null,
|
|
79
|
+
text: { format: { type: "text" } },
|
|
80
|
+
tool_choice: "auto",
|
|
81
|
+
tools: [],
|
|
82
|
+
top_p: null,
|
|
83
|
+
truncation: "disabled",
|
|
84
|
+
usage: null,
|
|
85
|
+
metadata: {},
|
|
86
|
+
};
|
|
87
|
+
let sequenceNumber = 0;
|
|
88
|
+
const event = (name: string, payload: Record<string, unknown>) =>
|
|
89
|
+
sse(name, { ...payload, sequence_number: sequenceNumber++ });
|
|
90
|
+
yield event("response.created", { type: "response.created", response });
|
|
91
|
+
yield event("response.in_progress", { type: "response.in_progress", response });
|
|
92
|
+
let outputIndex = 0;
|
|
93
|
+
let messageId: string | undefined;
|
|
94
|
+
let textValue = "";
|
|
95
|
+
const toolIndexes = new Map<string, number>();
|
|
96
|
+
let usage: LanguageModelUsage | undefined;
|
|
97
|
+
|
|
98
|
+
for await (const part of source) {
|
|
99
|
+
if (part.type === "text-start") {
|
|
100
|
+
messageId = `msg_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
101
|
+
yield event("response.output_item.added", {
|
|
102
|
+
type: "response.output_item.added",
|
|
103
|
+
output_index: outputIndex,
|
|
104
|
+
item: {
|
|
105
|
+
id: messageId,
|
|
106
|
+
type: "message",
|
|
107
|
+
role: "assistant",
|
|
108
|
+
status: "in_progress",
|
|
109
|
+
phase: "final_answer",
|
|
110
|
+
content: [],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
yield event("response.content_part.added", {
|
|
114
|
+
type: "response.content_part.added",
|
|
115
|
+
item_id: messageId,
|
|
116
|
+
output_index: outputIndex,
|
|
117
|
+
content_index: 0,
|
|
118
|
+
part: { type: "output_text", text: "", annotations: [], logprobs: [] },
|
|
119
|
+
});
|
|
120
|
+
} else if (part.type === "text-delta") {
|
|
121
|
+
textValue += part.text;
|
|
122
|
+
yield event("response.output_text.delta", {
|
|
123
|
+
type: "response.output_text.delta",
|
|
124
|
+
item_id: messageId,
|
|
125
|
+
output_index: outputIndex,
|
|
126
|
+
content_index: 0,
|
|
127
|
+
delta: part.text,
|
|
128
|
+
logprobs: [],
|
|
129
|
+
});
|
|
130
|
+
} else if (part.type === "text-end") {
|
|
131
|
+
yield event("response.output_text.done", {
|
|
132
|
+
type: "response.output_text.done",
|
|
133
|
+
item_id: messageId,
|
|
134
|
+
output_index: outputIndex,
|
|
135
|
+
content_index: 0,
|
|
136
|
+
text: textValue,
|
|
137
|
+
logprobs: [],
|
|
138
|
+
});
|
|
139
|
+
yield event("response.content_part.done", {
|
|
140
|
+
type: "response.content_part.done",
|
|
141
|
+
item_id: messageId,
|
|
142
|
+
output_index: outputIndex,
|
|
143
|
+
content_index: 0,
|
|
144
|
+
part: { type: "output_text", text: textValue, annotations: [], logprobs: [] },
|
|
145
|
+
});
|
|
146
|
+
const item = {
|
|
147
|
+
id: messageId,
|
|
148
|
+
type: "message",
|
|
149
|
+
role: "assistant",
|
|
150
|
+
status: "completed",
|
|
151
|
+
phase: "final_answer",
|
|
152
|
+
content: [{ type: "output_text", text: textValue, annotations: [], logprobs: [] }],
|
|
153
|
+
};
|
|
154
|
+
output.push(item);
|
|
155
|
+
yield event("response.output_item.done", {
|
|
156
|
+
type: "response.output_item.done",
|
|
157
|
+
output_index: outputIndex,
|
|
158
|
+
item,
|
|
159
|
+
});
|
|
160
|
+
outputIndex++;
|
|
161
|
+
textValue = "";
|
|
162
|
+
} else if (part.type === "reasoning-delta") {
|
|
163
|
+
yield event("response.reasoning_summary_text.delta", {
|
|
164
|
+
type: "response.reasoning_summary_text.delta",
|
|
165
|
+
output_index: outputIndex,
|
|
166
|
+
delta: part.text,
|
|
167
|
+
});
|
|
168
|
+
} else if (part.type === "tool-input-start") {
|
|
169
|
+
toolIndexes.set(part.id, outputIndex);
|
|
170
|
+
yield event("response.output_item.added", {
|
|
171
|
+
type: "response.output_item.added",
|
|
172
|
+
output_index: outputIndex,
|
|
173
|
+
item: {
|
|
174
|
+
id: `fc_${part.id}`,
|
|
175
|
+
type: "function_call",
|
|
176
|
+
call_id: part.id,
|
|
177
|
+
name: part.toolName,
|
|
178
|
+
arguments: "",
|
|
179
|
+
status: "in_progress",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
outputIndex++;
|
|
183
|
+
} else if (part.type === "tool-input-delta") {
|
|
184
|
+
yield event("response.function_call_arguments.delta", {
|
|
185
|
+
type: "response.function_call_arguments.delta",
|
|
186
|
+
item_id: `fc_${part.id}`,
|
|
187
|
+
output_index: toolIndexes.get(part.id) ?? outputIndex,
|
|
188
|
+
delta: part.delta,
|
|
189
|
+
});
|
|
190
|
+
} else if (part.type === "tool-call") {
|
|
191
|
+
const index = toolIndexes.get(part.toolCallId) ?? outputIndex++;
|
|
192
|
+
const item = {
|
|
193
|
+
id: `fc_${part.toolCallId}`,
|
|
194
|
+
type: "function_call",
|
|
195
|
+
call_id: part.toolCallId,
|
|
196
|
+
name: part.toolName,
|
|
197
|
+
arguments: JSON.stringify(part.input),
|
|
198
|
+
status: "completed",
|
|
199
|
+
};
|
|
200
|
+
yield event("response.function_call_arguments.done", {
|
|
201
|
+
type: "response.function_call_arguments.done",
|
|
202
|
+
item_id: `fc_${part.toolCallId}`,
|
|
203
|
+
output_index: index,
|
|
204
|
+
arguments: JSON.stringify(part.input),
|
|
205
|
+
});
|
|
206
|
+
yield event("response.output_item.done", {
|
|
207
|
+
type: "response.output_item.done",
|
|
208
|
+
output_index: index,
|
|
209
|
+
item,
|
|
210
|
+
});
|
|
211
|
+
output.push(item);
|
|
212
|
+
} else if (part.type === "finish") {
|
|
213
|
+
usage = part.totalUsage;
|
|
214
|
+
} else if (part.type === "error") {
|
|
215
|
+
yield event("error", protocolError(part.error));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
yield event("response.completed", {
|
|
219
|
+
type: "response.completed",
|
|
220
|
+
response: {
|
|
221
|
+
...response,
|
|
222
|
+
status: "completed",
|
|
223
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
224
|
+
usage: responsesUsage(usage),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function* chatEvents(
|
|
230
|
+
model: string,
|
|
231
|
+
source: AsyncIterable<TextStreamPart<ToolSet>>,
|
|
232
|
+
): AsyncGenerator<string> {
|
|
233
|
+
const id = `chatcmpl-${crypto.randomUUID().replaceAll("-", "")}`;
|
|
234
|
+
const created = Math.floor(Date.now() / 1000);
|
|
235
|
+
yield data({
|
|
236
|
+
id,
|
|
237
|
+
object: "chat.completion.chunk",
|
|
238
|
+
created,
|
|
239
|
+
model,
|
|
240
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
|
|
241
|
+
});
|
|
242
|
+
const toolIndexes = new Map<string, number>();
|
|
243
|
+
let nextToolIndex = 0;
|
|
244
|
+
for await (const part of source) {
|
|
245
|
+
if (part.type === "text-delta") {
|
|
246
|
+
yield data({
|
|
247
|
+
id,
|
|
248
|
+
object: "chat.completion.chunk",
|
|
249
|
+
created,
|
|
250
|
+
model,
|
|
251
|
+
choices: [{ index: 0, delta: { content: part.text }, finish_reason: null }],
|
|
252
|
+
});
|
|
253
|
+
} else if (part.type === "tool-input-start") {
|
|
254
|
+
const index = nextToolIndex++;
|
|
255
|
+
toolIndexes.set(part.id, index);
|
|
256
|
+
yield data({
|
|
257
|
+
id,
|
|
258
|
+
object: "chat.completion.chunk",
|
|
259
|
+
created,
|
|
260
|
+
model,
|
|
261
|
+
choices: [
|
|
262
|
+
{
|
|
263
|
+
index: 0,
|
|
264
|
+
delta: {
|
|
265
|
+
tool_calls: [
|
|
266
|
+
{
|
|
267
|
+
index,
|
|
268
|
+
id: part.id,
|
|
269
|
+
type: "function",
|
|
270
|
+
function: { name: part.toolName, arguments: "" },
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
finish_reason: null,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
} else if (part.type === "tool-input-delta") {
|
|
279
|
+
yield data({
|
|
280
|
+
id,
|
|
281
|
+
object: "chat.completion.chunk",
|
|
282
|
+
created,
|
|
283
|
+
model,
|
|
284
|
+
choices: [
|
|
285
|
+
{
|
|
286
|
+
index: 0,
|
|
287
|
+
delta: {
|
|
288
|
+
tool_calls: [
|
|
289
|
+
{
|
|
290
|
+
index: toolIndexes.get(part.id) ?? 0,
|
|
291
|
+
function: { arguments: part.delta },
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
finish_reason: null,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
} else if (part.type === "finish") {
|
|
300
|
+
yield data({
|
|
301
|
+
id,
|
|
302
|
+
object: "chat.completion.chunk",
|
|
303
|
+
created,
|
|
304
|
+
model,
|
|
305
|
+
choices: [{ index: 0, delta: {}, finish_reason: chatFinishReason(part.finishReason) }],
|
|
306
|
+
usage: chatUsage(part.totalUsage),
|
|
307
|
+
});
|
|
308
|
+
} else if (part.type === "error") {
|
|
309
|
+
yield data(protocolError(part.error));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
yield "data: [DONE]\n\n";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function* anthropicEvents(
|
|
316
|
+
model: string,
|
|
317
|
+
source: AsyncIterable<TextStreamPart<ToolSet>>,
|
|
318
|
+
): AsyncGenerator<string> {
|
|
319
|
+
const id = `msg_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
320
|
+
yield sse("message_start", {
|
|
321
|
+
type: "message_start",
|
|
322
|
+
message: {
|
|
323
|
+
id,
|
|
324
|
+
type: "message",
|
|
325
|
+
role: "assistant",
|
|
326
|
+
model,
|
|
327
|
+
content: [],
|
|
328
|
+
stop_reason: null,
|
|
329
|
+
stop_sequence: null,
|
|
330
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
let contentIndex = 0;
|
|
334
|
+
const toolIndexes = new Map<string, number>();
|
|
335
|
+
for await (const part of source) {
|
|
336
|
+
if (part.type === "text-start") {
|
|
337
|
+
yield sse("content_block_start", {
|
|
338
|
+
type: "content_block_start",
|
|
339
|
+
index: contentIndex,
|
|
340
|
+
content_block: { type: "text", text: "" },
|
|
341
|
+
});
|
|
342
|
+
} else if (part.type === "text-delta") {
|
|
343
|
+
yield sse("content_block_delta", {
|
|
344
|
+
type: "content_block_delta",
|
|
345
|
+
index: contentIndex,
|
|
346
|
+
delta: { type: "text_delta", text: part.text },
|
|
347
|
+
});
|
|
348
|
+
} else if (part.type === "text-end") {
|
|
349
|
+
yield sse("content_block_stop", { type: "content_block_stop", index: contentIndex++ });
|
|
350
|
+
} else if (part.type === "tool-input-start") {
|
|
351
|
+
toolIndexes.set(part.id, contentIndex);
|
|
352
|
+
yield sse("content_block_start", {
|
|
353
|
+
type: "content_block_start",
|
|
354
|
+
index: contentIndex++,
|
|
355
|
+
content_block: { type: "tool_use", id: part.id, name: part.toolName, input: {} },
|
|
356
|
+
});
|
|
357
|
+
} else if (part.type === "tool-input-delta") {
|
|
358
|
+
yield sse("content_block_delta", {
|
|
359
|
+
type: "content_block_delta",
|
|
360
|
+
index: toolIndexes.get(part.id) ?? 0,
|
|
361
|
+
delta: { type: "input_json_delta", partial_json: part.delta },
|
|
362
|
+
});
|
|
363
|
+
} else if (part.type === "tool-input-end") {
|
|
364
|
+
yield sse("content_block_stop", {
|
|
365
|
+
type: "content_block_stop",
|
|
366
|
+
index: toolIndexes.get(part.id) ?? 0,
|
|
367
|
+
});
|
|
368
|
+
} else if (part.type === "finish") {
|
|
369
|
+
yield sse("message_delta", {
|
|
370
|
+
type: "message_delta",
|
|
371
|
+
delta: { stop_reason: anthropicFinishReason(part.finishReason), stop_sequence: null },
|
|
372
|
+
usage: { output_tokens: part.totalUsage.outputTokens ?? 0 },
|
|
373
|
+
});
|
|
374
|
+
} else if (part.type === "error") {
|
|
375
|
+
yield sse("error", protocolError(part.error));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
yield sse("message_stop", { type: "message_stop" });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function responsesObject(model: string, generation: CompletedGeneration): Record<string, unknown> {
|
|
382
|
+
const output: Record<string, unknown>[] = [];
|
|
383
|
+
if (generation.text) {
|
|
384
|
+
output.push({
|
|
385
|
+
id: `msg_${crypto.randomUUID().replaceAll("-", "")}`,
|
|
386
|
+
type: "message",
|
|
387
|
+
role: "assistant",
|
|
388
|
+
status: "completed",
|
|
389
|
+
content: [{ type: "output_text", text: generation.text, annotations: [] }],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
for (const call of generation.toolCalls) {
|
|
393
|
+
output.push({
|
|
394
|
+
id: `fc_${call.toolCallId}`,
|
|
395
|
+
type: "function_call",
|
|
396
|
+
call_id: call.toolCallId,
|
|
397
|
+
name: call.toolName,
|
|
398
|
+
arguments: JSON.stringify(call.input),
|
|
399
|
+
status: "completed",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
id: `resp_${crypto.randomUUID().replaceAll("-", "")}`,
|
|
404
|
+
object: "response",
|
|
405
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
406
|
+
status: "completed",
|
|
407
|
+
model,
|
|
408
|
+
output,
|
|
409
|
+
usage: responsesUsage(generation.usage),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function chatObject(model: string, generation: CompletedGeneration): Record<string, unknown> {
|
|
414
|
+
return {
|
|
415
|
+
id: `chatcmpl-${crypto.randomUUID().replaceAll("-", "")}`,
|
|
416
|
+
object: "chat.completion",
|
|
417
|
+
created: Math.floor(Date.now() / 1000),
|
|
418
|
+
model,
|
|
419
|
+
choices: [
|
|
420
|
+
{
|
|
421
|
+
index: 0,
|
|
422
|
+
message: {
|
|
423
|
+
role: "assistant",
|
|
424
|
+
content: generation.text || null,
|
|
425
|
+
...(generation.toolCalls.length > 0
|
|
426
|
+
? {
|
|
427
|
+
tool_calls: generation.toolCalls.map((call) => ({
|
|
428
|
+
id: call.toolCallId,
|
|
429
|
+
type: "function",
|
|
430
|
+
function: { name: call.toolName, arguments: JSON.stringify(call.input) },
|
|
431
|
+
})),
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
},
|
|
435
|
+
finish_reason: chatFinishReason(generation.finishReason),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
usage: chatUsage(generation.usage),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function anthropicObject(model: string, generation: CompletedGeneration): Record<string, unknown> {
|
|
443
|
+
return {
|
|
444
|
+
id: `msg_${crypto.randomUUID().replaceAll("-", "")}`,
|
|
445
|
+
type: "message",
|
|
446
|
+
role: "assistant",
|
|
447
|
+
model,
|
|
448
|
+
content: [
|
|
449
|
+
...(generation.text ? [{ type: "text", text: generation.text }] : []),
|
|
450
|
+
...generation.toolCalls.map((call) => ({
|
|
451
|
+
type: "tool_use",
|
|
452
|
+
id: call.toolCallId,
|
|
453
|
+
name: call.toolName,
|
|
454
|
+
input: call.input,
|
|
455
|
+
})),
|
|
456
|
+
],
|
|
457
|
+
stop_reason: anthropicFinishReason(generation.finishReason),
|
|
458
|
+
stop_sequence: null,
|
|
459
|
+
usage: {
|
|
460
|
+
input_tokens: generation.usage.inputTokens ?? 0,
|
|
461
|
+
output_tokens: generation.usage.outputTokens ?? 0,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function responsesUsage(usage: LanguageModelUsage | undefined): Record<string, unknown> {
|
|
467
|
+
const input = usage?.inputTokens ?? 0;
|
|
468
|
+
const output = usage?.outputTokens ?? 0;
|
|
469
|
+
return {
|
|
470
|
+
input_tokens: input,
|
|
471
|
+
output_tokens: output,
|
|
472
|
+
total_tokens: input + output,
|
|
473
|
+
input_tokens_details: { cached_tokens: usage?.inputTokenDetails.cacheReadTokens ?? 0 },
|
|
474
|
+
output_tokens_details: {
|
|
475
|
+
reasoning_tokens: usage?.outputTokenDetails.reasoningTokens ?? 0,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function chatUsage(usage: LanguageModelUsage): Record<string, number> {
|
|
481
|
+
const input = usage.inputTokens ?? 0;
|
|
482
|
+
const output = usage.outputTokens ?? 0;
|
|
483
|
+
return { prompt_tokens: input, completion_tokens: output, total_tokens: input + output };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function chatFinishReason(reason: FinishReason): string {
|
|
487
|
+
if (reason === "tool-calls") return "tool_calls";
|
|
488
|
+
if (reason === "length") return "length";
|
|
489
|
+
if (reason === "content-filter") return "content_filter";
|
|
490
|
+
return "stop";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function anthropicFinishReason(reason: FinishReason): string {
|
|
494
|
+
if (reason === "tool-calls") return "tool_use";
|
|
495
|
+
if (reason === "length") return "max_tokens";
|
|
496
|
+
return "end_turn";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function protocolError(error: unknown): Record<string, unknown> {
|
|
500
|
+
return {
|
|
501
|
+
type: "error",
|
|
502
|
+
error: {
|
|
503
|
+
type: "api_error",
|
|
504
|
+
message: error instanceof Error ? error.message : String(error),
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function sse(event: string, payload: unknown): string {
|
|
510
|
+
return `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function data(payload: unknown): string {
|
|
514
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
515
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning-effort adaptation for gateway round trips.
|
|
3
|
+
*
|
|
4
|
+
* Proactively remaps client efforts onto the known ladder, then on a single
|
|
5
|
+
* unsupported-value 400 learns from the error and retries once with the
|
|
6
|
+
* updated ladder.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
learnReasoningLevelsFromError,
|
|
13
|
+
modelReasoningLevelsFor,
|
|
14
|
+
} from "@dbx-tools/model/metadata";
|
|
15
|
+
import {
|
|
16
|
+
adaptRequestReasoning,
|
|
17
|
+
parseReasoningLevels,
|
|
18
|
+
} from "@dbx-tools/model/reasoning-translation";
|
|
19
|
+
import { log } from "@dbx-tools/shared-core";
|
|
20
|
+
|
|
21
|
+
const logger = log.logger("appkit/model-gateway/reasoning");
|
|
22
|
+
|
|
23
|
+
/** Result of the first proactive adaptation pass. */
|
|
24
|
+
export interface AdaptedInferenceBody {
|
|
25
|
+
readonly body: Readonly<Record<string, unknown>>;
|
|
26
|
+
readonly wireEffort?: string;
|
|
27
|
+
readonly changed: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Adapt a request body onto the current known ladder for `model`. */
|
|
31
|
+
export function adaptInferenceReasoning(
|
|
32
|
+
model: string,
|
|
33
|
+
body: Readonly<Record<string, unknown>>,
|
|
34
|
+
): AdaptedInferenceBody {
|
|
35
|
+
const adapted = adaptRequestReasoning(body, modelReasoningLevelsFor(model));
|
|
36
|
+
if (adapted.changed) {
|
|
37
|
+
logger.debug("adapted reasoning effort before upstream", {
|
|
38
|
+
model,
|
|
39
|
+
effort: adapted.wireEffort,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
body: adapted.body,
|
|
44
|
+
wireEffort: adapted.wireEffort,
|
|
45
|
+
changed: adapted.changed,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* On an unsupported-reasoning 400, learn once and optionally produce a
|
|
51
|
+
* remapped retry body. Returns `undefined` when no retry should occur.
|
|
52
|
+
*/
|
|
53
|
+
export async function learnAndAdaptReasoningRetry(input: {
|
|
54
|
+
readonly model: string;
|
|
55
|
+
readonly body: Readonly<Record<string, unknown>>;
|
|
56
|
+
readonly response: Response;
|
|
57
|
+
readonly previousWireEffort?: string;
|
|
58
|
+
}): Promise<Readonly<Record<string, unknown>> | undefined> {
|
|
59
|
+
if (input.response.ok || input.response.status !== 400) return undefined;
|
|
60
|
+
|
|
61
|
+
let errorBody: unknown;
|
|
62
|
+
try {
|
|
63
|
+
errorBody = JSON.parse(await input.response.clone().text());
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (parseReasoningLevels(errorBody).length === 0) return undefined;
|
|
69
|
+
|
|
70
|
+
const levels = await learnReasoningLevelsFromError(input.model, errorBody);
|
|
71
|
+
logger.info("learned reasoning levels from upstream error", {
|
|
72
|
+
model: input.model,
|
|
73
|
+
levels,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const adapted = adaptRequestReasoning(input.body, levels);
|
|
77
|
+
if (adapted.wireEffort === undefined || adapted.wireEffort === input.previousWireEffort) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.info("retrying with adapted reasoning effort", {
|
|
82
|
+
model: input.model,
|
|
83
|
+
from: input.previousWireEffort,
|
|
84
|
+
to: adapted.wireEffort,
|
|
85
|
+
});
|
|
86
|
+
return adapted.body;
|
|
87
|
+
}
|