@codeworksh/aikit 0.3.1 → 0.4.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/README.md +73 -81
- package/{agent → dist/pack/agent}/codemode/drivers/drivers.d.mts +1 -1
- package/dist/pack/codemode-BEDXG7WN.d.mts +8736 -0
- package/{index.d.mts → dist/pack/index.d.mts} +1 -1
- package/dist/pack/index.mjs +8226 -0
- package/dist/pack/index.mjs.map +1 -0
- package/package.json +31 -24
- package/src/agent/agent.ts +790 -0
- package/src/agent/codemode/codemode.ts +561 -0
- package/src/agent/codemode/drivers/drivers.ts +1 -0
- package/src/agent/codemode/drivers/quickjs-wasi-driver.ts +149 -0
- package/src/agent/codemode/index.ts +1 -0
- package/src/agent/loop.ts +903 -0
- package/src/agent.ts +11 -0
- package/src/event/event.ts +277 -0
- package/src/index.ts +6 -0
- package/src/llm.ts +31 -0
- package/src/message/message.ts +311 -0
- package/src/model/catalog.ts +94 -0
- package/src/model/model.ts +250 -0
- package/src/model/transform.ts +59 -0
- package/src/provider/provider.ts +60 -0
- package/src/provider/providers/anthropic/index.ts +792 -0
- package/src/provider/providers/github/github.ts +34 -0
- package/src/provider/providers/openai/completions.ts +953 -0
- package/src/provider/providers/utils.ts +7 -0
- package/src/provider/register.ts +6 -0
- package/src/provider/stream.ts +278 -0
- package/src/stream.ts +72 -0
- package/src/utils/eventstream.ts +95 -0
- package/src/utils/helpers.ts +24 -0
- package/src/utils/jsonparse.ts +32 -0
- package/src/utils/overflow.ts +121 -0
- package/src/utils/sanitize.ts +25 -0
- package/src/utils/validation.ts +87 -0
- package/LICENSE +0 -21
- package/codemode-SiETGR3z.d.mts +0 -8410
- package/index.mjs +0 -3381
- package/index.mjs.map +0 -1
- /package/{agent → dist/pack/agent}/codemode/drivers/drivers.mjs +0 -0
- /package/{agent → dist/pack/agent}/codemode/drivers/drivers.mjs.map +0 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type {
|
|
3
|
+
ChatCompletionAssistantMessageParam,
|
|
4
|
+
ChatCompletionContentPart,
|
|
5
|
+
ChatCompletionContentPartImage,
|
|
6
|
+
ChatCompletionContentPartText,
|
|
7
|
+
ChatCompletionMessageParam,
|
|
8
|
+
ChatCompletionToolMessageParam,
|
|
9
|
+
} from "openai/resources/chat/completions.js";
|
|
10
|
+
import { Message } from "../../../message/message";
|
|
11
|
+
import { Model } from "../../../model/model";
|
|
12
|
+
import { AssistantMessageEventStream } from "../../../utils/eventstream";
|
|
13
|
+
import { parseStreamingJson } from "../../../utils/jsonparse";
|
|
14
|
+
import { sanitizeSurrogates } from "../../../utils/sanitize";
|
|
15
|
+
import { Provider } from "../../provider";
|
|
16
|
+
import { Stream } from "../../stream";
|
|
17
|
+
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "../github/github";
|
|
18
|
+
import { getEnvApiKey } from "../utils";
|
|
19
|
+
|
|
20
|
+
type OpenAIReasoningEffort = keyof Provider.StrictReasoningEffortMap;
|
|
21
|
+
|
|
22
|
+
type StreamingToolCallBlock = Message.ToolCall & {
|
|
23
|
+
index?: number;
|
|
24
|
+
partialArgs?: string;
|
|
25
|
+
streamIndex?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type StreamingBlock =
|
|
29
|
+
| (Message.TextContent & { index?: number })
|
|
30
|
+
| (Message.ThinkingContent & { index?: number })
|
|
31
|
+
| StreamingToolCallBlock;
|
|
32
|
+
|
|
33
|
+
function hasToolHistory(messages: Message.Message[]): boolean {
|
|
34
|
+
for (const msg of messages) {
|
|
35
|
+
if (msg.role !== "assistant") continue;
|
|
36
|
+
if (msg.parts.some((block) => block.type === "toolCall")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildSyntheticToolResult(
|
|
44
|
+
block: Extract<Message.ToolCall, { status: "pending" | "running" }>,
|
|
45
|
+
compat: Required<Model.OpenAICompletionsCompat>,
|
|
46
|
+
): ChatCompletionToolMessageParam {
|
|
47
|
+
const message: ChatCompletionToolMessageParam = {
|
|
48
|
+
role: "tool",
|
|
49
|
+
content: ["<result>", "No Result Provided", "</result>"].join("\n"),
|
|
50
|
+
tool_call_id: block.callID,
|
|
51
|
+
};
|
|
52
|
+
if (compat.requiresToolResultName) {
|
|
53
|
+
(message as ChatCompletionToolMessageParam & { name?: string }).name = block.name;
|
|
54
|
+
}
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface OpenAICompletionsOptions extends Stream.Options {
|
|
59
|
+
toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } };
|
|
60
|
+
reasoningEffort?: OpenAIReasoningEffort;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveBaseUrl(model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>): string {
|
|
64
|
+
return model.baseUrl || (model.provider.id === Provider.KnownProviderEnum.openai ? "https://api.openai.com/v1" : "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const streamOpenAICompletions: Stream.StreamFunction<
|
|
68
|
+
typeof Model.KnownProtocolEnum.openaiCompletions,
|
|
69
|
+
OpenAICompletionsOptions
|
|
70
|
+
> = (
|
|
71
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
72
|
+
context: Message.Context,
|
|
73
|
+
options?: OpenAICompletionsOptions,
|
|
74
|
+
): AssistantMessageEventStream => {
|
|
75
|
+
const stream = new AssistantMessageEventStream();
|
|
76
|
+
|
|
77
|
+
void (async () => {
|
|
78
|
+
const output = Message.createAssistantMessage({
|
|
79
|
+
role: "assistant",
|
|
80
|
+
parts: [],
|
|
81
|
+
protocol: model.protocol,
|
|
82
|
+
provider: model.provider,
|
|
83
|
+
model: model.id,
|
|
84
|
+
usage: {
|
|
85
|
+
input: 0,
|
|
86
|
+
output: 0,
|
|
87
|
+
cacheRead: 0,
|
|
88
|
+
cacheWrite: 0,
|
|
89
|
+
totalTokens: 0,
|
|
90
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
91
|
+
},
|
|
92
|
+
stopReason: "stop",
|
|
93
|
+
time: {
|
|
94
|
+
created: Date.now(),
|
|
95
|
+
completed: Date.now(),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
101
|
+
const client = createClient(model, context, apiKey, options?.headers);
|
|
102
|
+
let params = buildParams(model, context, options);
|
|
103
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
104
|
+
if (nextParams !== undefined) {
|
|
105
|
+
params = nextParams as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
|
|
109
|
+
stream.push({ type: "start", partial: output });
|
|
110
|
+
|
|
111
|
+
let currentBlock: Exclude<StreamingBlock, StreamingToolCallBlock> | null = null;
|
|
112
|
+
const blocks = output.parts as StreamingBlock[];
|
|
113
|
+
const activeToolCalls = new Map<number, StreamingToolCallBlock>();
|
|
114
|
+
const finishCurrentBlock = (block?: StreamingBlock | null) => {
|
|
115
|
+
if (block) {
|
|
116
|
+
const partIndex = block.index ?? blocks.indexOf(block);
|
|
117
|
+
delete block.index;
|
|
118
|
+
|
|
119
|
+
if (block.type === "text") {
|
|
120
|
+
stream.push({
|
|
121
|
+
type: "text.end",
|
|
122
|
+
partIndex,
|
|
123
|
+
content: block.text,
|
|
124
|
+
partial: output,
|
|
125
|
+
});
|
|
126
|
+
} else if (block.type === "thinking") {
|
|
127
|
+
stream.push({
|
|
128
|
+
type: "thinking.end",
|
|
129
|
+
partIndex,
|
|
130
|
+
content: block.thinking,
|
|
131
|
+
partial: output,
|
|
132
|
+
});
|
|
133
|
+
} else if (block.type === "toolCall") {
|
|
134
|
+
if (block.streamIndex !== undefined) {
|
|
135
|
+
activeToolCalls.delete(block.streamIndex);
|
|
136
|
+
}
|
|
137
|
+
block.arguments = parseStreamingJson<Record<string, unknown>>(block.partialArgs);
|
|
138
|
+
delete block.partialArgs;
|
|
139
|
+
delete block.streamIndex;
|
|
140
|
+
stream.push({
|
|
141
|
+
type: "toolcall.end",
|
|
142
|
+
partIndex,
|
|
143
|
+
toolCall: block,
|
|
144
|
+
partial: output,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const flushActiveToolCalls = () => {
|
|
150
|
+
for (const toolBlock of [...activeToolCalls.values()].sort((a, b) => (a.index ?? 0) - (b.index ?? 0))) {
|
|
151
|
+
finishCurrentBlock(toolBlock);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for await (const chunk of openaiStream) {
|
|
156
|
+
if (!chunk || typeof chunk !== "object") continue;
|
|
157
|
+
|
|
158
|
+
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
|
|
159
|
+
// and each chunk in a streamed completion carries the same id.
|
|
160
|
+
output.responseId ||= chunk.id;
|
|
161
|
+
if (chunk.usage) {
|
|
162
|
+
output.usage = parseChunkUsage(chunk.usage, model);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
|
|
166
|
+
if (!choice) continue;
|
|
167
|
+
|
|
168
|
+
// fallback: some providers (e.g., Moonshot) return usage
|
|
169
|
+
// in choice.usage instead of the standard chunk.usage
|
|
170
|
+
const choiceWithUsage = choice as unknown as {
|
|
171
|
+
usage?: Parameters<typeof parseChunkUsage>[0];
|
|
172
|
+
};
|
|
173
|
+
if (!chunk.usage && choiceWithUsage.usage) {
|
|
174
|
+
output.usage = parseChunkUsage(choiceWithUsage.usage, model);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (choice.finish_reason) {
|
|
178
|
+
const finishReasonResult = mapStopReason(choice.finish_reason);
|
|
179
|
+
output.stopReason = finishReasonResult.stopReason;
|
|
180
|
+
if (finishReasonResult.errorMessage) {
|
|
181
|
+
output.errorMessage = finishReasonResult.errorMessage;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!choice.delta) continue;
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
choice.delta.content !== null &&
|
|
189
|
+
choice.delta.content !== undefined &&
|
|
190
|
+
choice.delta.content.length > 0
|
|
191
|
+
) {
|
|
192
|
+
if (activeToolCalls.size > 0) {
|
|
193
|
+
flushActiveToolCalls();
|
|
194
|
+
}
|
|
195
|
+
if (!currentBlock || currentBlock.type !== "text") {
|
|
196
|
+
finishCurrentBlock(currentBlock);
|
|
197
|
+
currentBlock = { type: "text", text: "", index: output.parts.length };
|
|
198
|
+
output.parts.push(currentBlock);
|
|
199
|
+
stream.push({
|
|
200
|
+
type: "text.start",
|
|
201
|
+
partIndex: currentBlock.index!,
|
|
202
|
+
partial: output,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (currentBlock.type === "text") {
|
|
207
|
+
currentBlock.text += choice.delta.content;
|
|
208
|
+
stream.push({
|
|
209
|
+
type: "text.delta",
|
|
210
|
+
partIndex: currentBlock.index!,
|
|
211
|
+
delta: choice.delta.content,
|
|
212
|
+
partial: output,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// some endpoints return reasoning in reasoning_content (llama.cpp),
|
|
218
|
+
// or reasoning (other openai compatible endpoints)
|
|
219
|
+
// Use the first non-empty reasoning field to avoid duplication
|
|
220
|
+
// (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
|
|
221
|
+
const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"] as const;
|
|
222
|
+
let foundReasoningField: (typeof reasoningFields)[number] | null = null;
|
|
223
|
+
for (const field of reasoningFields) {
|
|
224
|
+
const value = (choice.delta as Record<string, unknown>)[field];
|
|
225
|
+
if (typeof value === "string" && value.length > 0) {
|
|
226
|
+
foundReasoningField = field;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (foundReasoningField) {
|
|
232
|
+
if (activeToolCalls.size > 0) {
|
|
233
|
+
flushActiveToolCalls();
|
|
234
|
+
}
|
|
235
|
+
if (!currentBlock || currentBlock.type !== "thinking") {
|
|
236
|
+
finishCurrentBlock(currentBlock);
|
|
237
|
+
currentBlock = {
|
|
238
|
+
type: "thinking",
|
|
239
|
+
thinking: "",
|
|
240
|
+
thinkingSignature: foundReasoningField,
|
|
241
|
+
index: output.parts.length,
|
|
242
|
+
};
|
|
243
|
+
output.parts.push(currentBlock);
|
|
244
|
+
stream.push({
|
|
245
|
+
type: "thinking.start",
|
|
246
|
+
partIndex: currentBlock.index!,
|
|
247
|
+
partial: output,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (currentBlock.type === "thinking") {
|
|
252
|
+
const delta = (choice.delta as Record<string, unknown>)[foundReasoningField];
|
|
253
|
+
if (typeof delta === "string") {
|
|
254
|
+
currentBlock.thinking += delta;
|
|
255
|
+
stream.push({
|
|
256
|
+
type: "thinking.delta",
|
|
257
|
+
partIndex: currentBlock.index!,
|
|
258
|
+
delta,
|
|
259
|
+
partial: output,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (choice.delta.tool_calls) {
|
|
266
|
+
if (currentBlock) {
|
|
267
|
+
finishCurrentBlock(currentBlock);
|
|
268
|
+
currentBlock = null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
272
|
+
const streamIndex = toolCall.index ?? activeToolCalls.size;
|
|
273
|
+
let toolBlock = activeToolCalls.get(streamIndex);
|
|
274
|
+
if (!toolBlock) {
|
|
275
|
+
toolBlock = {
|
|
276
|
+
type: "toolCall",
|
|
277
|
+
callID: toolCall.id || "",
|
|
278
|
+
name: toolCall.function?.name || "",
|
|
279
|
+
arguments: {},
|
|
280
|
+
partialArgs: "",
|
|
281
|
+
index: output.parts.length,
|
|
282
|
+
streamIndex,
|
|
283
|
+
status: "pending",
|
|
284
|
+
time: {
|
|
285
|
+
start: Date.now(),
|
|
286
|
+
end: Date.now(),
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
activeToolCalls.set(streamIndex, toolBlock);
|
|
290
|
+
output.parts.push(toolBlock);
|
|
291
|
+
stream.push({
|
|
292
|
+
type: "toolcall.start",
|
|
293
|
+
partIndex: toolBlock.index!,
|
|
294
|
+
partial: output,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (toolCall.id) toolBlock.callID = toolCall.id;
|
|
299
|
+
if (toolCall.function?.name) toolBlock.name = toolCall.function.name;
|
|
300
|
+
|
|
301
|
+
let delta = "";
|
|
302
|
+
if (toolCall.function?.arguments) {
|
|
303
|
+
delta = toolCall.function.arguments;
|
|
304
|
+
toolBlock.partialArgs = (toolBlock.partialArgs || "") + delta;
|
|
305
|
+
toolBlock.arguments = parseStreamingJson<Record<string, unknown>>(toolBlock.partialArgs);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
toolBlock.time.end = Date.now();
|
|
309
|
+
stream.push({
|
|
310
|
+
type: "toolcall.delta",
|
|
311
|
+
partIndex: toolBlock.index!,
|
|
312
|
+
delta,
|
|
313
|
+
partial: output,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const reasoningDetails = (choice.delta as { reasoning_details?: Array<Record<string, unknown>> })
|
|
319
|
+
.reasoning_details;
|
|
320
|
+
if (reasoningDetails && Array.isArray(reasoningDetails)) {
|
|
321
|
+
for (const detail of reasoningDetails) {
|
|
322
|
+
if (
|
|
323
|
+
detail.type === "reasoning.encrypted" &&
|
|
324
|
+
typeof detail.id === "string" &&
|
|
325
|
+
detail.data !== undefined
|
|
326
|
+
) {
|
|
327
|
+
const matchingToolCall = output.parts.find(
|
|
328
|
+
(block) => block.type === "toolCall" && block.callID === detail.id,
|
|
329
|
+
) as Message.ToolCall | undefined;
|
|
330
|
+
if (matchingToolCall) {
|
|
331
|
+
matchingToolCall.thoughtSignature = JSON.stringify(detail);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
finishCurrentBlock(currentBlock);
|
|
339
|
+
flushActiveToolCalls();
|
|
340
|
+
if (options?.signal?.aborted) {
|
|
341
|
+
throw new Error("Request was aborted");
|
|
342
|
+
}
|
|
343
|
+
if (output.stopReason === "aborted") {
|
|
344
|
+
throw new Error("Request was aborted");
|
|
345
|
+
}
|
|
346
|
+
if (output.stopReason === "error") {
|
|
347
|
+
throw new Error(output.errorMessage || "Provider returned an error stop reason");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
output.time.completed = Date.now();
|
|
351
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
352
|
+
stream.end();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
for (const block of output.parts) {
|
|
355
|
+
delete (block as StreamingBlock).index;
|
|
356
|
+
delete (block as StreamingToolCallBlock).partialArgs;
|
|
357
|
+
delete (block as StreamingToolCallBlock).streamIndex;
|
|
358
|
+
}
|
|
359
|
+
output.time.completed = Date.now();
|
|
360
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
361
|
+
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
362
|
+
const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
|
|
363
|
+
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
364
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
365
|
+
stream.end();
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
|
|
369
|
+
return stream;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const streamSimpleOpenAICompletions: Stream.StreamFunction<
|
|
373
|
+
typeof Model.KnownProtocolEnum.openaiCompletions,
|
|
374
|
+
Stream.SimpleOptions
|
|
375
|
+
> = (
|
|
376
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
377
|
+
context: Message.Context,
|
|
378
|
+
options?: Stream.SimpleOptions,
|
|
379
|
+
): AssistantMessageEventStream => {
|
|
380
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
381
|
+
if (!apiKey) {
|
|
382
|
+
throw new Error(`No API key for provider: ${model.provider.id}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const base = Stream.buildBaseOptions(model, options, apiKey);
|
|
386
|
+
const simpleReasoning = Model.supportsXhigh(model) ? options?.reasoning : Stream.clampReasoning(options?.reasoning);
|
|
387
|
+
const reasoningEffort: OpenAIReasoningEffort | undefined =
|
|
388
|
+
simpleReasoning && simpleReasoning !== Stream.ThinkingLevelEnum.off ? simpleReasoning : undefined;
|
|
389
|
+
const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice;
|
|
390
|
+
|
|
391
|
+
return streamOpenAICompletions(model, context, {
|
|
392
|
+
...base,
|
|
393
|
+
reasoningEffort,
|
|
394
|
+
toolChoice,
|
|
395
|
+
} satisfies OpenAICompletionsOptions);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
function buildParams(
|
|
399
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
400
|
+
context: Message.Context,
|
|
401
|
+
options?: OpenAICompletionsOptions,
|
|
402
|
+
) {
|
|
403
|
+
const compat = getCompat(model);
|
|
404
|
+
const messages = convertMessages(model, context, compat);
|
|
405
|
+
maybeAddOpenRouterAnthropicCacheControl(model, messages);
|
|
406
|
+
//
|
|
407
|
+
// resolve URL
|
|
408
|
+
// ideally this is last resort
|
|
409
|
+
const baseUrl = resolveBaseUrl(model);
|
|
410
|
+
|
|
411
|
+
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
|
412
|
+
model: model.id,
|
|
413
|
+
messages,
|
|
414
|
+
stream: true,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (compat.supportsUsageInStreaming !== false) {
|
|
418
|
+
(params as typeof params & { stream_options?: { include_usage: boolean } }).stream_options = {
|
|
419
|
+
include_usage: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (compat.supportsStore) {
|
|
424
|
+
params.store = false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options?.maxTokens) {
|
|
428
|
+
if (compat.maxTokensField === "max_tokens") {
|
|
429
|
+
(params as typeof params & { max_tokens?: number }).max_tokens = options.maxTokens;
|
|
430
|
+
} else {
|
|
431
|
+
params.max_completion_tokens = options.maxTokens;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (options?.temperature !== undefined) {
|
|
436
|
+
params.temperature = options.temperature;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (context.tools) {
|
|
440
|
+
params.tools = convertTools(context.tools, compat);
|
|
441
|
+
if (compat.zaiToolStream) {
|
|
442
|
+
(params as typeof params & { tool_stream?: boolean }).tool_stream = true;
|
|
443
|
+
}
|
|
444
|
+
} else if (hasToolHistory(context.messages)) {
|
|
445
|
+
params.tools = [];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (options?.toolChoice) {
|
|
449
|
+
params.tool_choice = options.toolChoice;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if ((compat.thinkingFormat === "zai" || compat.thinkingFormat === "qwen") && model.reasoning) {
|
|
453
|
+
(params as typeof params & { enable_thinking?: boolean }).enable_thinking = !!options?.reasoningEffort;
|
|
454
|
+
} else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
|
|
455
|
+
(params as typeof params & { chat_template_kwargs?: { enable_thinking: boolean } }).chat_template_kwargs = {
|
|
456
|
+
enable_thinking: !!options?.reasoningEffort,
|
|
457
|
+
};
|
|
458
|
+
} else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
|
|
459
|
+
const openRouterParams = params as typeof params & { reasoning?: { effort?: string } };
|
|
460
|
+
if (options?.reasoningEffort) {
|
|
461
|
+
openRouterParams.reasoning = {
|
|
462
|
+
effort: mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap),
|
|
463
|
+
};
|
|
464
|
+
} else {
|
|
465
|
+
openRouterParams.reasoning = { effort: "none" };
|
|
466
|
+
}
|
|
467
|
+
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
|
|
468
|
+
(params as { reasoning_effort?: string }).reasoning_effort = mapReasoningEffort(
|
|
469
|
+
options.reasoningEffort,
|
|
470
|
+
compat.reasoningEffortMap,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
|
|
475
|
+
(params as typeof params & { provider?: Provider.OpenRouterRouting }).provider = model.compat.openRouterRouting;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
|
|
479
|
+
const routing = model.compat.vercelGatewayRouting;
|
|
480
|
+
if (routing.only || routing.order) {
|
|
481
|
+
const gatewayOptions: Record<string, string[]> = {};
|
|
482
|
+
if (routing.only) gatewayOptions.only = routing.only;
|
|
483
|
+
if (routing.order) gatewayOptions.order = routing.order;
|
|
484
|
+
(
|
|
485
|
+
params as typeof params & {
|
|
486
|
+
providerOptions?: { gateway: Record<string, string[]> };
|
|
487
|
+
}
|
|
488
|
+
).providerOptions = { gateway: gatewayOptions };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return params;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function createClient(
|
|
496
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
497
|
+
context: Message.Context,
|
|
498
|
+
apiKey?: string,
|
|
499
|
+
optsHeaders?: Record<string, string>,
|
|
500
|
+
) {
|
|
501
|
+
const baseUrl = resolveBaseUrl(model);
|
|
502
|
+
if (!apiKey) {
|
|
503
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"OpenAI API key is required. set OPENAI_API_KEY environment variable or pass it as an argument",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const headers = { ...model.headers };
|
|
512
|
+
if (model.provider.id === Provider.KnownProviderEnum.githubCopilot) {
|
|
513
|
+
const hasImages = hasCopilotVisionInput(context.messages);
|
|
514
|
+
const copilotHeaders = buildCopilotDynamicHeaders({
|
|
515
|
+
messages: context.messages,
|
|
516
|
+
hasImages,
|
|
517
|
+
});
|
|
518
|
+
Object.assign(headers, copilotHeaders);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (optsHeaders) {
|
|
522
|
+
Object.assign(headers, optsHeaders);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return new OpenAI({
|
|
526
|
+
apiKey,
|
|
527
|
+
baseURL: baseUrl,
|
|
528
|
+
dangerouslyAllowBrowser: true,
|
|
529
|
+
defaultHeaders: headers,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function mapReasoningEffort(
|
|
534
|
+
effort: NonNullable<OpenAICompletionsOptions["reasoningEffort"]>,
|
|
535
|
+
reasoningEffortMap: Partial<Record<OpenAIReasoningEffort, string>>,
|
|
536
|
+
): string {
|
|
537
|
+
return reasoningEffortMap[effort] ?? effort;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function maybeAddOpenRouterAnthropicCacheControl(
|
|
541
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
542
|
+
messages: ChatCompletionMessageParam[],
|
|
543
|
+
): void {
|
|
544
|
+
if (model.provider.id !== Provider.KnownProviderEnum.openrouter || !model.id.startsWith("anthropic/")) return;
|
|
545
|
+
|
|
546
|
+
// Anthropic-style caching requires cache_control on a text part. Add a breakpoint
|
|
547
|
+
// on the last user/assistant message (walking backwards until we find text content).
|
|
548
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
549
|
+
const msg = messages[i];
|
|
550
|
+
if (!msg) continue;
|
|
551
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
552
|
+
|
|
553
|
+
const content = msg.content;
|
|
554
|
+
if (typeof content === "string") {
|
|
555
|
+
msg.content = [
|
|
556
|
+
Object.assign({ type: "text" as const, text: content }, { cache_control: { type: "ephemeral" } }),
|
|
557
|
+
];
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!Array.isArray(content)) continue;
|
|
562
|
+
|
|
563
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
564
|
+
const part = content[j];
|
|
565
|
+
if (part?.type === "text") {
|
|
566
|
+
Object.assign(part, { cache_control: { type: "ephemeral" } });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function convertMessages(
|
|
574
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
575
|
+
context: Message.Context,
|
|
576
|
+
compat: Required<Model.OpenAICompletionsCompat>,
|
|
577
|
+
): ChatCompletionMessageParam[] {
|
|
578
|
+
const params: ChatCompletionMessageParam[] = [];
|
|
579
|
+
|
|
580
|
+
const normalizeToolCallId = (id: string): string => {
|
|
581
|
+
// handle pipe-separated IDs from OpenAI Responses API
|
|
582
|
+
// format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
|
|
583
|
+
// these come from providers like github-copilot, openai-codex, opencode
|
|
584
|
+
// extract just the call_id part and normalize it
|
|
585
|
+
if (id.includes("|")) {
|
|
586
|
+
const callID = id.split("|")[0] ?? id;
|
|
587
|
+
// sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
|
|
588
|
+
return callID.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (model.provider.id === Provider.KnownProviderEnum.openai) {
|
|
592
|
+
return id.length > 40 ? id.slice(0, 40) : id;
|
|
593
|
+
}
|
|
594
|
+
return id;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const transformedMessages = Message.transformMessages(context.messages, model, normalizeToolCallId);
|
|
598
|
+
|
|
599
|
+
if (context.systemPrompt) {
|
|
600
|
+
const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
|
|
601
|
+
const role = useDeveloperRole ? "developer" : "system";
|
|
602
|
+
params.push({
|
|
603
|
+
role,
|
|
604
|
+
content: sanitizeSurrogates(context.systemPrompt),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let lastRole: "user" | "assistant" | "tool" | null = null;
|
|
609
|
+
|
|
610
|
+
for (const msg of transformedMessages) {
|
|
611
|
+
// some providers don't allow user messages directly after tool results
|
|
612
|
+
// insert a synthetic assistant message to bridge the gap
|
|
613
|
+
if (compat.requiresAssistantAfterToolResult && lastRole === "tool" && msg.role === "user") {
|
|
614
|
+
params.push({
|
|
615
|
+
role: "assistant",
|
|
616
|
+
content: "I have processed the tool results.",
|
|
617
|
+
});
|
|
618
|
+
lastRole = "assistant";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (msg.role === "user") {
|
|
622
|
+
const content: ChatCompletionContentPart[] = msg.parts.flatMap((item): ChatCompletionContentPart[] => {
|
|
623
|
+
if (item.type === "text") {
|
|
624
|
+
const text = sanitizeSurrogates(item.text);
|
|
625
|
+
if (text.trim().length === 0) return [];
|
|
626
|
+
return [
|
|
627
|
+
{
|
|
628
|
+
type: "text",
|
|
629
|
+
text,
|
|
630
|
+
} satisfies ChatCompletionContentPartText,
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!model.input.includes("image")) return [];
|
|
635
|
+
return [
|
|
636
|
+
{
|
|
637
|
+
type: "image_url",
|
|
638
|
+
image_url: {
|
|
639
|
+
url: `data:${item.mimeType};base64,${item.data}`,
|
|
640
|
+
},
|
|
641
|
+
} satisfies ChatCompletionContentPartImage,
|
|
642
|
+
];
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (content.length === 0) continue;
|
|
646
|
+
|
|
647
|
+
params.push({
|
|
648
|
+
role: "user",
|
|
649
|
+
content,
|
|
650
|
+
});
|
|
651
|
+
lastRole = "user";
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const assistantMsg: ChatCompletionAssistantMessageParam = {
|
|
656
|
+
role: "assistant",
|
|
657
|
+
content: compat.requiresAssistantAfterToolResult ? "" : null,
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const textBlocks = msg.parts.filter((block) => block.type === "text") as Message.TextContent[];
|
|
661
|
+
const textSegments = textBlocks
|
|
662
|
+
.map((block) => sanitizeSurrogates(block.text))
|
|
663
|
+
.filter((text) => text.trim().length > 0);
|
|
664
|
+
|
|
665
|
+
const thinkingBlocks = msg.parts.filter((block) => block.type === "thinking") as Message.ThinkingContent[];
|
|
666
|
+
const nonEmptyThinkingBlocks = thinkingBlocks.filter((block) => block.thinking.trim().length > 0);
|
|
667
|
+
|
|
668
|
+
if (compat.requiresThinkingAsText && nonEmptyThinkingBlocks.length > 0) {
|
|
669
|
+
textSegments.unshift(...nonEmptyThinkingBlocks.map((block) => sanitizeSurrogates(block.thinking)));
|
|
670
|
+
} else if (nonEmptyThinkingBlocks.length > 0) {
|
|
671
|
+
const signature = nonEmptyThinkingBlocks[0]?.thinkingSignature;
|
|
672
|
+
if (signature && signature.length > 0) {
|
|
673
|
+
(assistantMsg as ChatCompletionAssistantMessageParam & Record<string, string>)[signature] =
|
|
674
|
+
nonEmptyThinkingBlocks.map((block) => sanitizeSurrogates(block.thinking)).join("\n");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (textSegments.length > 0) {
|
|
679
|
+
assistantMsg.content = textSegments.join("");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const toolCalls = msg.parts.filter((block) => block.type === "toolCall") as Message.ToolCall[];
|
|
683
|
+
if (toolCalls.length > 0) {
|
|
684
|
+
assistantMsg.tool_calls = toolCalls.map((toolCall) => ({
|
|
685
|
+
id: toolCall.callID,
|
|
686
|
+
type: "function" as const,
|
|
687
|
+
function: {
|
|
688
|
+
name: toolCall.name,
|
|
689
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
690
|
+
},
|
|
691
|
+
}));
|
|
692
|
+
|
|
693
|
+
const reasoningDetails = toolCalls
|
|
694
|
+
.filter((toolCall) => toolCall.thoughtSignature)
|
|
695
|
+
.map((toolCall) => {
|
|
696
|
+
try {
|
|
697
|
+
return JSON.parse(toolCall.thoughtSignature!);
|
|
698
|
+
} catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
.filter(Boolean);
|
|
703
|
+
|
|
704
|
+
if (reasoningDetails.length > 0) {
|
|
705
|
+
(
|
|
706
|
+
assistantMsg as ChatCompletionAssistantMessageParam & { reasoning_details?: unknown[] }
|
|
707
|
+
).reasoning_details = reasoningDetails;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const assistantContent = assistantMsg.content;
|
|
712
|
+
const hasAssistantContent =
|
|
713
|
+
assistantContent !== null &&
|
|
714
|
+
assistantContent !== undefined &&
|
|
715
|
+
(typeof assistantContent === "string" ? assistantContent.length > 0 : assistantContent.length > 0);
|
|
716
|
+
if (!hasAssistantContent && !assistantMsg.tool_calls) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
params.push(assistantMsg);
|
|
721
|
+
lastRole = "assistant";
|
|
722
|
+
|
|
723
|
+
const imageBlocks: Array<{ type: "image_url"; image_url: { url: string } }> = [];
|
|
724
|
+
|
|
725
|
+
for (const toolCall of toolCalls) {
|
|
726
|
+
if (toolCall.status === "pending" || toolCall.status === "running") {
|
|
727
|
+
params.push(buildSyntheticToolResult(toolCall, compat));
|
|
728
|
+
lastRole = "tool";
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const textResult = toolCall.result.content
|
|
733
|
+
.filter((content) => content.type === "text")
|
|
734
|
+
.map((content) => content.text)
|
|
735
|
+
.join("\n");
|
|
736
|
+
const hasImages = toolCall.result.content.some((content) => content.type === "image");
|
|
737
|
+
|
|
738
|
+
const toolResultMsg: ChatCompletionToolMessageParam = {
|
|
739
|
+
role: "tool",
|
|
740
|
+
content: sanitizeSurrogates(textResult.length > 0 ? textResult : "(see attached image)"),
|
|
741
|
+
tool_call_id: toolCall.callID,
|
|
742
|
+
};
|
|
743
|
+
if (compat.requiresToolResultName) {
|
|
744
|
+
(toolResultMsg as ChatCompletionToolMessageParam & { name?: string }).name = toolCall.name;
|
|
745
|
+
}
|
|
746
|
+
params.push(toolResultMsg);
|
|
747
|
+
lastRole = "tool";
|
|
748
|
+
|
|
749
|
+
if (hasImages && model.input.includes("image")) {
|
|
750
|
+
for (const block of toolCall.result.content) {
|
|
751
|
+
if (block.type === "image") {
|
|
752
|
+
imageBlocks.push({
|
|
753
|
+
type: "image_url",
|
|
754
|
+
image_url: {
|
|
755
|
+
url: `data:${block.mimeType};base64,${block.data}`,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (imageBlocks.length > 0) {
|
|
764
|
+
if (compat.requiresAssistantAfterToolResult) {
|
|
765
|
+
params.push({
|
|
766
|
+
role: "assistant",
|
|
767
|
+
content: "I have processed the tool results.",
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
params.push({
|
|
772
|
+
role: "user",
|
|
773
|
+
content: [
|
|
774
|
+
{
|
|
775
|
+
type: "text",
|
|
776
|
+
text: "Attached image(s) from tool result:",
|
|
777
|
+
},
|
|
778
|
+
...imageBlocks,
|
|
779
|
+
],
|
|
780
|
+
});
|
|
781
|
+
lastRole = "user";
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return params;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function convertTools(
|
|
789
|
+
tools: Message.Tool[],
|
|
790
|
+
compat: Required<Model.OpenAICompletionsCompat>,
|
|
791
|
+
): OpenAI.Chat.Completions.ChatCompletionTool[] {
|
|
792
|
+
return tools.map((tool) => ({
|
|
793
|
+
type: "function",
|
|
794
|
+
function: {
|
|
795
|
+
name: tool.name,
|
|
796
|
+
description: tool.description,
|
|
797
|
+
parameters: tool.parameters as Record<string, unknown>,
|
|
798
|
+
...(compat.supportsStrictMode !== false && { strict: false }),
|
|
799
|
+
},
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function parseChunkUsage(
|
|
804
|
+
rawUsage: {
|
|
805
|
+
prompt_tokens?: number;
|
|
806
|
+
completion_tokens?: number;
|
|
807
|
+
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
|
808
|
+
completion_tokens_details?: { reasoning_tokens?: number };
|
|
809
|
+
},
|
|
810
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
811
|
+
): Message.AssistantMessage["usage"] {
|
|
812
|
+
const promptTokens = rawUsage.prompt_tokens || 0;
|
|
813
|
+
const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens || 0;
|
|
814
|
+
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
|
|
815
|
+
const reasoningTokens = rawUsage.completion_tokens_details?.reasoning_tokens || 0;
|
|
816
|
+
|
|
817
|
+
const cacheReadTokens =
|
|
818
|
+
cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
|
|
819
|
+
|
|
820
|
+
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
|
821
|
+
const outputTokens = (rawUsage.completion_tokens || 0) + reasoningTokens;
|
|
822
|
+
const usage: Message.AssistantMessage["usage"] = {
|
|
823
|
+
input,
|
|
824
|
+
output: outputTokens,
|
|
825
|
+
cacheRead: cacheReadTokens,
|
|
826
|
+
cacheWrite: cacheWriteTokens,
|
|
827
|
+
totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
828
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
829
|
+
};
|
|
830
|
+
Model.calculateCost(model, usage);
|
|
831
|
+
return usage;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function mapStopReason(reason: string | null): {
|
|
835
|
+
stopReason: Message.StopReason;
|
|
836
|
+
errorMessage?: string;
|
|
837
|
+
} {
|
|
838
|
+
if (reason === null) return { stopReason: "stop" };
|
|
839
|
+
switch (reason) {
|
|
840
|
+
case "stop":
|
|
841
|
+
case "end":
|
|
842
|
+
return { stopReason: "stop" };
|
|
843
|
+
case "length":
|
|
844
|
+
return { stopReason: "length" };
|
|
845
|
+
case "function_call":
|
|
846
|
+
case "tool_calls":
|
|
847
|
+
return { stopReason: "toolUse" };
|
|
848
|
+
case "content_filter":
|
|
849
|
+
return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
|
|
850
|
+
case "network_error":
|
|
851
|
+
return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
|
|
852
|
+
default:
|
|
853
|
+
return {
|
|
854
|
+
stopReason: "error",
|
|
855
|
+
errorMessage: `Provider finish_reason: ${reason}`,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function detectCompat(
|
|
861
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
862
|
+
): Required<Model.OpenAICompletionsCompat> {
|
|
863
|
+
const provider = model.provider.id;
|
|
864
|
+
const baseUrl = resolveBaseUrl(model);
|
|
865
|
+
|
|
866
|
+
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
|
|
867
|
+
const isNonStandard =
|
|
868
|
+
provider === "cerebras" ||
|
|
869
|
+
baseUrl.includes("cerebras.ai") ||
|
|
870
|
+
provider === "xai" ||
|
|
871
|
+
baseUrl.includes("api.x.ai") ||
|
|
872
|
+
baseUrl.includes("chutes.ai") ||
|
|
873
|
+
baseUrl.includes("deepseek.com") ||
|
|
874
|
+
isZai ||
|
|
875
|
+
provider === "opencode" ||
|
|
876
|
+
baseUrl.includes("opencode.ai");
|
|
877
|
+
|
|
878
|
+
const useMaxTokens = baseUrl.includes("chutes.ai");
|
|
879
|
+
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
|
880
|
+
const isGroq = provider === "groq" || baseUrl.includes("groq.com");
|
|
881
|
+
|
|
882
|
+
const reasoningEffortMap: Partial<Record<OpenAIReasoningEffort, string>> =
|
|
883
|
+
isGroq && model.id === "qwen/qwen3-32b"
|
|
884
|
+
? {
|
|
885
|
+
minimal: "default",
|
|
886
|
+
low: "default",
|
|
887
|
+
medium: "default",
|
|
888
|
+
high: "default",
|
|
889
|
+
xhigh: "default",
|
|
890
|
+
}
|
|
891
|
+
: {};
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
supportsStore: !isNonStandard,
|
|
895
|
+
supportsDeveloperRole: !isNonStandard,
|
|
896
|
+
supportsReasoningEffort: !isGrok && !isZai,
|
|
897
|
+
reasoningEffortMap,
|
|
898
|
+
supportsUsageInStreaming: true,
|
|
899
|
+
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
|
900
|
+
requiresToolResultName: false,
|
|
901
|
+
requiresAssistantAfterToolResult: false,
|
|
902
|
+
requiresThinkingAsText: false,
|
|
903
|
+
thinkingFormat: isZai
|
|
904
|
+
? "zai"
|
|
905
|
+
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
|
906
|
+
? "openrouter"
|
|
907
|
+
: "openai",
|
|
908
|
+
openRouterRouting: {},
|
|
909
|
+
vercelGatewayRouting: {},
|
|
910
|
+
zaiToolStream: false,
|
|
911
|
+
supportsStrictMode: true,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Get resolved compatibility settings for a model.
|
|
917
|
+
* Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
|
|
918
|
+
*/
|
|
919
|
+
function getCompat(
|
|
920
|
+
model: Model.TModel<typeof Model.KnownProtocolEnum.openaiCompletions>,
|
|
921
|
+
): Required<Model.OpenAICompletionsCompat> {
|
|
922
|
+
const detected = detectCompat(model);
|
|
923
|
+
if (!model.compat) return detected;
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
supportsStore: model.compat.supportsStore ?? detected.supportsStore,
|
|
927
|
+
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
|
|
928
|
+
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
|
|
929
|
+
reasoningEffortMap: model.compat.reasoningEffortMap ?? detected.reasoningEffortMap,
|
|
930
|
+
supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
|
|
931
|
+
maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
|
|
932
|
+
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
|
|
933
|
+
requiresAssistantAfterToolResult:
|
|
934
|
+
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
|
|
935
|
+
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
|
|
936
|
+
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
|
937
|
+
openRouterRouting: model.compat.openRouterRouting ?? {},
|
|
938
|
+
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
|
939
|
+
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
|
940
|
+
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const openAICompletionsProtocolProvider: Stream.ProtocolProvider<
|
|
945
|
+
typeof Model.KnownProtocolEnum.openaiCompletions,
|
|
946
|
+
OpenAICompletionsOptions
|
|
947
|
+
> = {
|
|
948
|
+
protocol: Model.KnownProtocolEnum.openaiCompletions,
|
|
949
|
+
stream: streamOpenAICompletions,
|
|
950
|
+
streamSimple: streamSimpleOpenAICompletions,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
export default openAICompletionsProtocolProvider;
|