@fleetagent/pi-ai 0.1.8 → 0.2.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -24
- package/dist/cli.js.map +1 -1
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +1 -1
- package/dist/env-api-keys.js.map +1 -1
- package/dist/image-models.generated.d.ts +225 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +227 -2
- package/dist/image-models.generated.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +37 -2
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +6120 -2593
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +5347 -2110
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +259 -9
- package/dist/models.js.map +1 -1
- package/dist/oauth.d.ts +6 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +4 -1
- package/dist/oauth.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts +5 -4
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +303 -330
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +492 -499
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/azure-openai-responses.d.ts +4 -2
- package/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.js +52 -35
- package/dist/providers/azure-openai-responses.js.map +1 -1
- package/dist/providers/faux.d.ts +19 -22
- package/dist/providers/faux.d.ts.map +1 -1
- package/dist/providers/faux.js +106 -70
- package/dist/providers/faux.js.map +1 -1
- package/dist/providers/github-copilot-headers.d.ts +7 -4
- package/dist/providers/github-copilot-headers.d.ts.map +1 -1
- package/dist/providers/github-copilot-headers.js.map +1 -1
- package/dist/providers/google-shared.d.ts +24 -5
- package/dist/providers/google-shared.d.ts.map +1 -1
- package/dist/providers/google-shared.js +139 -140
- package/dist/providers/google-shared.js.map +1 -1
- package/dist/providers/google-vertex.d.ts +3 -7
- package/dist/providers/google-vertex.d.ts.map +1 -1
- package/dist/providers/google-vertex.js +216 -219
- package/dist/providers/google-vertex.js.map +1 -1
- package/dist/providers/google.d.ts +4 -7
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +214 -218
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/images/openrouter.d.ts.map +1 -1
- package/dist/providers/images/openrouter.js +37 -27
- package/dist/providers/images/openrouter.js.map +1 -1
- package/dist/providers/mistral.d.ts +7 -6
- package/dist/providers/mistral.d.ts.map +1 -1
- package/dist/providers/mistral.js +287 -265
- package/dist/providers/mistral.js.map +1 -1
- package/dist/providers/openai-codex-responses.d.ts +8 -5
- package/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/dist/providers/openai-codex-responses.js +493 -391
- package/dist/providers/openai-codex-responses.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +7 -10
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +670 -664
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-shared.d.ts +5 -4
- package/dist/providers/openai-responses-shared.d.ts.map +1 -1
- package/dist/providers/openai-responses-shared.js +390 -360
- package/dist/providers/openai-responses-shared.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +5 -5
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +134 -109
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/simple-options.d.ts +4 -2
- package/dist/providers/simple-options.d.ts.map +1 -1
- package/dist/providers/simple-options.js.map +1 -1
- package/dist/providers/transform-messages.d.ts.map +1 -1
- package/dist/providers/transform-messages.js +112 -137
- package/dist/providers/transform-messages.js.map +1 -1
- package/dist/stream.d.ts +0 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +0 -1
- package/dist/stream.js.map +1 -1
- package/dist/types.d.ts +91 -63
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/error-body.d.ts.map +1 -1
- package/dist/utils/error-body.js +15 -4
- package/dist/utils/error-body.js.map +1 -1
- package/dist/utils/estimate.d.ts +4 -0
- package/dist/utils/estimate.d.ts.map +1 -1
- package/dist/utils/estimate.js +3 -1
- package/dist/utils/estimate.js.map +1 -1
- package/dist/utils/headers.d.ts +3 -0
- package/dist/utils/headers.d.ts.map +1 -1
- package/dist/utils/headers.js +21 -0
- package/dist/utils/headers.js.map +1 -1
- package/dist/utils/json-parse.d.ts.map +1 -1
- package/dist/utils/json-parse.js +17 -19
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/oauth/anthropic.d.ts +4 -11
- package/dist/utils/oauth/anthropic.d.ts.map +1 -1
- package/dist/utils/oauth/anthropic.js +100 -117
- package/dist/utils/oauth/anthropic.js.map +1 -1
- package/dist/utils/oauth/device-code.d.ts.map +1 -1
- package/dist/utils/oauth/device-code.js +51 -38
- package/dist/utils/oauth/device-code.js.map +1 -1
- package/dist/utils/oauth/github-copilot.d.ts +6 -9
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/dist/utils/oauth/github-copilot.js +47 -42
- package/dist/utils/oauth/github-copilot.js.map +1 -1
- package/dist/utils/oauth/index.d.ts +8 -17
- package/dist/utils/oauth/index.d.ts.map +1 -1
- package/dist/utils/oauth/index.js +27 -23
- package/dist/utils/oauth/index.js.map +1 -1
- package/dist/utils/oauth/oauth-page.d.ts.map +1 -1
- package/dist/utils/oauth/oauth-page.js.map +1 -1
- package/dist/utils/oauth/openai-codex.d.ts +7 -12
- package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
- package/dist/utils/oauth/openai-codex.js +72 -89
- package/dist/utils/oauth/openai-codex.js.map +1 -1
- package/dist/utils/oauth/pkce.d.ts +5 -4
- package/dist/utils/oauth/pkce.d.ts.map +1 -1
- package/dist/utils/oauth/pkce.js.map +1 -1
- package/dist/utils/oauth/types.d.ts +7 -2
- package/dist/utils/oauth/types.d.ts.map +1 -1
- package/dist/utils/oauth/types.js.map +1 -1
- package/dist/utils/provider-retry.d.ts +16 -0
- package/dist/utils/provider-retry.d.ts.map +1 -0
- package/dist/utils/provider-retry.js +94 -0
- package/dist/utils/provider-retry.js.map +1 -0
- package/dist/utils/retry.d.ts +14 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +84 -0
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/typebox-helpers.d.ts +5 -4
- package/dist/utils/typebox-helpers.d.ts.map +1 -1
- package/dist/utils/typebox-helpers.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +102 -99
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,8 +3,10 @@ import { getEnvApiKey } from "../env-api-keys.js";
|
|
|
3
3
|
import { calculateCost, clampThinkingLevel } from "../models.js";
|
|
4
4
|
import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
|
|
5
5
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
6
|
+
import { shortHash } from "../utils/hash.js";
|
|
6
7
|
import { headersToRecord } from "../utils/headers.js";
|
|
7
8
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
|
9
|
+
import { retryProviderRequest } from "../utils/provider-retry.js";
|
|
8
10
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|
9
11
|
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
10
12
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
@@ -22,7 +24,9 @@ function hasToolHistory(messages) {
|
|
|
22
24
|
return true;
|
|
23
25
|
}
|
|
24
26
|
if (msg.role === "assistant") {
|
|
25
|
-
|
|
27
|
+
// This helper reads the original context after conversion, so retain the
|
|
28
|
+
// transform boundary's tolerance for untyped null or missing content.
|
|
29
|
+
if (msg.content?.some((block) => block.type === "toolCall")) {
|
|
26
30
|
return true;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
@@ -50,289 +54,285 @@ function resolveCacheRetention(cacheRetention) {
|
|
|
50
54
|
}
|
|
51
55
|
return "short";
|
|
52
56
|
}
|
|
57
|
+
function createOpenAICompletionsOutput(model) {
|
|
58
|
+
return {
|
|
59
|
+
role: "assistant",
|
|
60
|
+
content: [],
|
|
61
|
+
api: model.api,
|
|
62
|
+
provider: model.provider,
|
|
63
|
+
model: model.id,
|
|
64
|
+
usage: {
|
|
65
|
+
input: 0,
|
|
66
|
+
output: 0,
|
|
67
|
+
cacheRead: 0,
|
|
68
|
+
cacheWrite: 0,
|
|
69
|
+
totalTokens: 0,
|
|
70
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
71
|
+
},
|
|
72
|
+
stopReason: "stop",
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function startOpenAICompletionsRequest(model, context, options) {
|
|
77
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
78
|
+
const compat = getCompat(model);
|
|
79
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
80
|
+
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
81
|
+
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
|
|
82
|
+
let params = buildParams(model, context, options, compat, cacheRetention);
|
|
83
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
84
|
+
if (nextParams !== undefined)
|
|
85
|
+
params = nextParams;
|
|
86
|
+
const requestOptions = {
|
|
87
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
88
|
+
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
89
|
+
maxRetries: 0,
|
|
90
|
+
};
|
|
91
|
+
const { data, response } = await retryProviderRequest(() => client.chat.completions
|
|
92
|
+
.create(params, requestOptions)
|
|
93
|
+
.withResponse(), {
|
|
94
|
+
maxRetries: options?.maxRetries,
|
|
95
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
96
|
+
signal: options?.signal,
|
|
97
|
+
});
|
|
98
|
+
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
function createOpenAICompletionStreamState(model, output, stream) {
|
|
102
|
+
return {
|
|
103
|
+
model,
|
|
104
|
+
output,
|
|
105
|
+
stream,
|
|
106
|
+
blocks: output.content,
|
|
107
|
+
textBlock: null,
|
|
108
|
+
thinkingBlock: null,
|
|
109
|
+
hasFinishReason: false,
|
|
110
|
+
toolCallBlocksByIndex: new Map(),
|
|
111
|
+
toolCallBlocksById: new Map(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function openAICompletionContentIndex(state, block) {
|
|
115
|
+
return state.blocks.indexOf(block);
|
|
116
|
+
}
|
|
117
|
+
function finishOpenAICompletionBlock(state, block) {
|
|
118
|
+
const contentIndex = openAICompletionContentIndex(state, block);
|
|
119
|
+
if (contentIndex === -1)
|
|
120
|
+
return;
|
|
121
|
+
if (block.type === "text") {
|
|
122
|
+
state.stream.push({ type: "text_end", contentIndex, content: block.text, partial: state.output });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (block.type === "thinking") {
|
|
126
|
+
state.stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: state.output });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
block.arguments = parseStreamingJson(block.partialArgs);
|
|
130
|
+
delete block.partialArgs;
|
|
131
|
+
delete block.streamIndex;
|
|
132
|
+
state.stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: state.output });
|
|
133
|
+
}
|
|
134
|
+
function appendOpenAITextDelta(state, delta) {
|
|
135
|
+
if (!state.textBlock) {
|
|
136
|
+
state.textBlock = { type: "text", text: "" };
|
|
137
|
+
state.blocks.push(state.textBlock);
|
|
138
|
+
state.stream.push({
|
|
139
|
+
type: "text_start",
|
|
140
|
+
contentIndex: openAICompletionContentIndex(state, state.textBlock),
|
|
141
|
+
partial: state.output,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
state.textBlock.text += delta;
|
|
145
|
+
state.stream.push({
|
|
146
|
+
type: "text_delta",
|
|
147
|
+
contentIndex: openAICompletionContentIndex(state, state.textBlock),
|
|
148
|
+
delta,
|
|
149
|
+
partial: state.output,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function appendOpenAIThinkingDelta(state, reasoning) {
|
|
153
|
+
if (!state.thinkingBlock) {
|
|
154
|
+
state.thinkingBlock = { type: "thinking", thinking: "", thinkingSignature: reasoning.signature };
|
|
155
|
+
state.blocks.push(state.thinkingBlock);
|
|
156
|
+
state.stream.push({
|
|
157
|
+
type: "thinking_start",
|
|
158
|
+
contentIndex: openAICompletionContentIndex(state, state.thinkingBlock),
|
|
159
|
+
partial: state.output,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
state.thinkingBlock.thinking += reasoning.delta;
|
|
163
|
+
state.stream.push({
|
|
164
|
+
type: "thinking_delta",
|
|
165
|
+
contentIndex: openAICompletionContentIndex(state, state.thinkingBlock),
|
|
166
|
+
delta: reasoning.delta,
|
|
167
|
+
partial: state.output,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function resolveOpenAIReasoningDelta(model, delta) {
|
|
171
|
+
const fields = ["reasoning_content", "reasoning", "reasoning_text"];
|
|
172
|
+
const deltaFields = delta;
|
|
173
|
+
for (const field of fields) {
|
|
174
|
+
const value = deltaFields[field];
|
|
175
|
+
if (typeof value !== "string" || value.length === 0)
|
|
176
|
+
continue;
|
|
177
|
+
return {
|
|
178
|
+
delta: value,
|
|
179
|
+
signature: model.provider === "opencode-go" && field === "reasoning" ? "reasoning_content" : field,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function ensureOpenAIToolCallBlock(state, toolCall) {
|
|
185
|
+
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
186
|
+
let block = streamIndex !== undefined ? state.toolCallBlocksByIndex.get(streamIndex) : undefined;
|
|
187
|
+
if (!block && toolCall.id)
|
|
188
|
+
block = state.toolCallBlocksById.get(toolCall.id);
|
|
189
|
+
if (!block) {
|
|
190
|
+
block = {
|
|
191
|
+
type: "toolCall",
|
|
192
|
+
id: toolCall.id || "",
|
|
193
|
+
name: toolCall.function?.name || "",
|
|
194
|
+
arguments: {},
|
|
195
|
+
partialArgs: "",
|
|
196
|
+
streamIndex,
|
|
197
|
+
};
|
|
198
|
+
if (streamIndex !== undefined)
|
|
199
|
+
state.toolCallBlocksByIndex.set(streamIndex, block);
|
|
200
|
+
if (toolCall.id)
|
|
201
|
+
state.toolCallBlocksById.set(toolCall.id, block);
|
|
202
|
+
state.blocks.push(block);
|
|
203
|
+
state.stream.push({
|
|
204
|
+
type: "toolcall_start",
|
|
205
|
+
contentIndex: openAICompletionContentIndex(state, block),
|
|
206
|
+
partial: state.output,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (streamIndex !== undefined && block.streamIndex === undefined) {
|
|
210
|
+
block.streamIndex = streamIndex;
|
|
211
|
+
state.toolCallBlocksByIndex.set(streamIndex, block);
|
|
212
|
+
}
|
|
213
|
+
if (toolCall.id)
|
|
214
|
+
state.toolCallBlocksById.set(toolCall.id, block);
|
|
215
|
+
return block;
|
|
216
|
+
}
|
|
217
|
+
function appendOpenAIToolCallDelta(state, toolCall) {
|
|
218
|
+
const block = ensureOpenAIToolCallBlock(state, toolCall);
|
|
219
|
+
if (!block.id && toolCall.id) {
|
|
220
|
+
block.id = toolCall.id;
|
|
221
|
+
state.toolCallBlocksById.set(toolCall.id, block);
|
|
222
|
+
}
|
|
223
|
+
if (!block.name && toolCall.function?.name)
|
|
224
|
+
block.name = toolCall.function.name;
|
|
225
|
+
let delta = "";
|
|
226
|
+
if (toolCall.function?.arguments) {
|
|
227
|
+
delta = toolCall.function.arguments;
|
|
228
|
+
block.partialArgs = (block.partialArgs ?? "") + delta;
|
|
229
|
+
block.arguments = parseStreamingJson(block.partialArgs);
|
|
230
|
+
}
|
|
231
|
+
state.stream.push({
|
|
232
|
+
type: "toolcall_delta",
|
|
233
|
+
contentIndex: openAICompletionContentIndex(state, block),
|
|
234
|
+
delta,
|
|
235
|
+
partial: state.output,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function applyOpenAIReasoningDetails(state, delta) {
|
|
239
|
+
const details = delta.reasoning_details;
|
|
240
|
+
if (!Array.isArray(details))
|
|
241
|
+
return;
|
|
242
|
+
for (const detail of details) {
|
|
243
|
+
if (detail.type !== "reasoning.encrypted" || typeof detail.id !== "string" || !detail.data)
|
|
244
|
+
continue;
|
|
245
|
+
const matchingToolCall = state.toolCallBlocksById.get(detail.id);
|
|
246
|
+
if (matchingToolCall)
|
|
247
|
+
matchingToolCall.thoughtSignature = JSON.stringify(detail);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function consumeOpenAICompletionDelta(state, delta) {
|
|
251
|
+
if (delta.content !== null && delta.content !== undefined && delta.content.length > 0) {
|
|
252
|
+
appendOpenAITextDelta(state, delta.content);
|
|
253
|
+
}
|
|
254
|
+
const reasoning = resolveOpenAIReasoningDelta(state.model, delta);
|
|
255
|
+
if (reasoning)
|
|
256
|
+
appendOpenAIThinkingDelta(state, reasoning);
|
|
257
|
+
for (const toolCall of delta.tool_calls ?? [])
|
|
258
|
+
appendOpenAIToolCallDelta(state, toolCall);
|
|
259
|
+
applyOpenAIReasoningDetails(state, delta);
|
|
260
|
+
}
|
|
261
|
+
function applyOpenAICompletionFinishReason(state, finishReason) {
|
|
262
|
+
if (!finishReason)
|
|
263
|
+
return;
|
|
264
|
+
const result = mapStopReason(finishReason);
|
|
265
|
+
state.output.stopReason = result.stopReason;
|
|
266
|
+
if (result.errorMessage)
|
|
267
|
+
state.output.errorMessage = result.errorMessage;
|
|
268
|
+
state.hasFinishReason = true;
|
|
269
|
+
}
|
|
270
|
+
function consumeOpenAICompletionChunk(state, chunk) {
|
|
271
|
+
state.output.responseId ||= chunk.id;
|
|
272
|
+
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== state.model.id) {
|
|
273
|
+
state.output.responseModel ||= chunk.model;
|
|
274
|
+
}
|
|
275
|
+
if (chunk.usage)
|
|
276
|
+
state.output.usage = parseChunkUsage(chunk.usage, state.model);
|
|
277
|
+
const choice = (Array.isArray(chunk.choices) ? chunk.choices[0] : undefined);
|
|
278
|
+
if (!choice)
|
|
279
|
+
return;
|
|
280
|
+
if (!chunk.usage && choice.usage)
|
|
281
|
+
state.output.usage = parseChunkUsage(choice.usage, state.model);
|
|
282
|
+
applyOpenAICompletionFinishReason(state, choice.finish_reason);
|
|
283
|
+
if (choice.delta)
|
|
284
|
+
consumeOpenAICompletionDelta(state, choice.delta);
|
|
285
|
+
}
|
|
286
|
+
async function consumeOpenAICompletionStream(state, openaiStream) {
|
|
287
|
+
for await (const chunk of openaiStream) {
|
|
288
|
+
if (chunk && typeof chunk === "object")
|
|
289
|
+
consumeOpenAICompletionChunk(state, chunk);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function finalizeOpenAICompletionStream(state, options) {
|
|
293
|
+
for (const block of state.blocks)
|
|
294
|
+
finishOpenAICompletionBlock(state, block);
|
|
295
|
+
if (options?.signal?.aborted || state.output.stopReason === "aborted")
|
|
296
|
+
throw new Error("Request was aborted");
|
|
297
|
+
if (state.output.stopReason === "error") {
|
|
298
|
+
throw new Error(state.output.errorMessage || "Provider returned an error stop reason");
|
|
299
|
+
}
|
|
300
|
+
if (!state.hasFinishReason || state.output.stopReason === "pending") {
|
|
301
|
+
throw new Error("Stream ended without finish_reason");
|
|
302
|
+
}
|
|
303
|
+
state.stream.push({ type: "done", reason: state.output.stopReason, message: state.output });
|
|
304
|
+
state.stream.end();
|
|
305
|
+
}
|
|
306
|
+
function failOpenAICompletionStream(output, stream, error, options) {
|
|
307
|
+
for (const block of output.content) {
|
|
308
|
+
delete block.index;
|
|
309
|
+
delete block.partialArgs;
|
|
310
|
+
delete block.streamIndex;
|
|
311
|
+
}
|
|
312
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
313
|
+
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
|
314
|
+
const rawMetadata = error?.error?.metadata?.raw;
|
|
315
|
+
if (typeof rawMetadata === "string" && rawMetadata.length > 0)
|
|
316
|
+
output.errorMessage += `\n${rawMetadata}`;
|
|
317
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
318
|
+
stream.end();
|
|
319
|
+
}
|
|
320
|
+
async function runOpenAICompletionsStream(model, context, options, stream) {
|
|
321
|
+
const output = createOpenAICompletionsOutput(model);
|
|
322
|
+
try {
|
|
323
|
+
const openaiStream = await startOpenAICompletionsRequest(model, context, options);
|
|
324
|
+
stream.push({ type: "start", partial: output });
|
|
325
|
+
const state = createOpenAICompletionStreamState(model, output, stream);
|
|
326
|
+
await consumeOpenAICompletionStream(state, openaiStream);
|
|
327
|
+
finalizeOpenAICompletionStream(state, options);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
failOpenAICompletionStream(output, stream, error, options);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
53
333
|
export const streamOpenAICompletions = (model, context, options) => {
|
|
54
334
|
const stream = new AssistantMessageEventStream();
|
|
55
|
-
|
|
56
|
-
const output = {
|
|
57
|
-
role: "assistant",
|
|
58
|
-
content: [],
|
|
59
|
-
api: model.api,
|
|
60
|
-
provider: model.provider,
|
|
61
|
-
model: model.id,
|
|
62
|
-
usage: {
|
|
63
|
-
input: 0,
|
|
64
|
-
output: 0,
|
|
65
|
-
cacheRead: 0,
|
|
66
|
-
cacheWrite: 0,
|
|
67
|
-
totalTokens: 0,
|
|
68
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
69
|
-
},
|
|
70
|
-
stopReason: "stop",
|
|
71
|
-
timestamp: Date.now(),
|
|
72
|
-
};
|
|
73
|
-
try {
|
|
74
|
-
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
75
|
-
const compat = getCompat(model);
|
|
76
|
-
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
77
|
-
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
78
|
-
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
|
|
79
|
-
let params = buildParams(model, context, options, compat, cacheRetention);
|
|
80
|
-
const nextParams = await options?.onPayload?.(params, model);
|
|
81
|
-
if (nextParams !== undefined) {
|
|
82
|
-
params = nextParams;
|
|
83
|
-
}
|
|
84
|
-
const requestOptions = {
|
|
85
|
-
...(options?.signal ? { signal: options.signal } : {}),
|
|
86
|
-
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
87
|
-
maxRetries: options?.maxRetries ?? 0,
|
|
88
|
-
};
|
|
89
|
-
const { data: openaiStream, response } = await client.chat.completions
|
|
90
|
-
.create(params, requestOptions)
|
|
91
|
-
.withResponse();
|
|
92
|
-
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
93
|
-
stream.push({ type: "start", partial: output });
|
|
94
|
-
let textBlock = null;
|
|
95
|
-
let thinkingBlock = null;
|
|
96
|
-
let hasFinishReason = false;
|
|
97
|
-
const toolCallBlocksByIndex = new Map();
|
|
98
|
-
const toolCallBlocksById = new Map();
|
|
99
|
-
const blocks = output.content;
|
|
100
|
-
const getContentIndex = (block) => blocks.indexOf(block);
|
|
101
|
-
const finishBlock = (block) => {
|
|
102
|
-
const contentIndex = getContentIndex(block);
|
|
103
|
-
if (contentIndex === -1) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (block.type === "text") {
|
|
107
|
-
stream.push({
|
|
108
|
-
type: "text_end",
|
|
109
|
-
contentIndex,
|
|
110
|
-
content: block.text,
|
|
111
|
-
partial: output,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
else if (block.type === "thinking") {
|
|
115
|
-
stream.push({
|
|
116
|
-
type: "thinking_end",
|
|
117
|
-
contentIndex,
|
|
118
|
-
content: block.thinking,
|
|
119
|
-
partial: output,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
else if (block.type === "toolCall") {
|
|
123
|
-
block.arguments = parseStreamingJson(block.partialArgs);
|
|
124
|
-
// Finalize in-place and strip the scratch buffers so replay only
|
|
125
|
-
// carries parsed arguments.
|
|
126
|
-
delete block.partialArgs;
|
|
127
|
-
delete block.streamIndex;
|
|
128
|
-
stream.push({
|
|
129
|
-
type: "toolcall_end",
|
|
130
|
-
contentIndex,
|
|
131
|
-
toolCall: block,
|
|
132
|
-
partial: output,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
const ensureTextBlock = () => {
|
|
137
|
-
if (!textBlock) {
|
|
138
|
-
textBlock = { type: "text", text: "" };
|
|
139
|
-
blocks.push(textBlock);
|
|
140
|
-
stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output });
|
|
141
|
-
}
|
|
142
|
-
return textBlock;
|
|
143
|
-
};
|
|
144
|
-
const ensureThinkingBlock = (thinkingSignature) => {
|
|
145
|
-
if (!thinkingBlock) {
|
|
146
|
-
thinkingBlock = {
|
|
147
|
-
type: "thinking",
|
|
148
|
-
thinking: "",
|
|
149
|
-
thinkingSignature,
|
|
150
|
-
};
|
|
151
|
-
blocks.push(thinkingBlock);
|
|
152
|
-
stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output });
|
|
153
|
-
}
|
|
154
|
-
return thinkingBlock;
|
|
155
|
-
};
|
|
156
|
-
const ensureToolCallBlock = (toolCall) => {
|
|
157
|
-
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
158
|
-
let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
|
|
159
|
-
if (!block && toolCall.id) {
|
|
160
|
-
block = toolCallBlocksById.get(toolCall.id);
|
|
161
|
-
}
|
|
162
|
-
if (!block) {
|
|
163
|
-
block = {
|
|
164
|
-
type: "toolCall",
|
|
165
|
-
id: toolCall.id || "",
|
|
166
|
-
name: toolCall.function?.name || "",
|
|
167
|
-
arguments: {},
|
|
168
|
-
partialArgs: "",
|
|
169
|
-
streamIndex,
|
|
170
|
-
};
|
|
171
|
-
if (streamIndex !== undefined) {
|
|
172
|
-
toolCallBlocksByIndex.set(streamIndex, block);
|
|
173
|
-
}
|
|
174
|
-
if (toolCall.id) {
|
|
175
|
-
toolCallBlocksById.set(toolCall.id, block);
|
|
176
|
-
}
|
|
177
|
-
blocks.push(block);
|
|
178
|
-
stream.push({
|
|
179
|
-
type: "toolcall_start",
|
|
180
|
-
contentIndex: getContentIndex(block),
|
|
181
|
-
partial: output,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
if (streamIndex !== undefined && block.streamIndex === undefined) {
|
|
185
|
-
block.streamIndex = streamIndex;
|
|
186
|
-
toolCallBlocksByIndex.set(streamIndex, block);
|
|
187
|
-
}
|
|
188
|
-
if (toolCall.id) {
|
|
189
|
-
toolCallBlocksById.set(toolCall.id, block);
|
|
190
|
-
}
|
|
191
|
-
return block;
|
|
192
|
-
};
|
|
193
|
-
for await (const chunk of openaiStream) {
|
|
194
|
-
if (!chunk || typeof chunk !== "object")
|
|
195
|
-
continue;
|
|
196
|
-
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
|
|
197
|
-
// and each chunk in a streamed completion carries the same id.
|
|
198
|
-
output.responseId ||= chunk.id;
|
|
199
|
-
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) {
|
|
200
|
-
output.responseModel ||= chunk.model;
|
|
201
|
-
}
|
|
202
|
-
if (chunk.usage) {
|
|
203
|
-
output.usage = parseChunkUsage(chunk.usage, model);
|
|
204
|
-
}
|
|
205
|
-
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
|
|
206
|
-
if (!choice)
|
|
207
|
-
continue;
|
|
208
|
-
// Fallback: some providers (e.g., Moonshot) return usage
|
|
209
|
-
// in choice.usage instead of the standard chunk.usage
|
|
210
|
-
if (!chunk.usage && choice.usage) {
|
|
211
|
-
output.usage = parseChunkUsage(choice.usage, model);
|
|
212
|
-
}
|
|
213
|
-
if (choice.finish_reason) {
|
|
214
|
-
const finishReasonResult = mapStopReason(choice.finish_reason);
|
|
215
|
-
output.stopReason = finishReasonResult.stopReason;
|
|
216
|
-
if (finishReasonResult.errorMessage) {
|
|
217
|
-
output.errorMessage = finishReasonResult.errorMessage;
|
|
218
|
-
}
|
|
219
|
-
hasFinishReason = true;
|
|
220
|
-
}
|
|
221
|
-
if (choice.delta) {
|
|
222
|
-
if (choice.delta.content !== null &&
|
|
223
|
-
choice.delta.content !== undefined &&
|
|
224
|
-
choice.delta.content.length > 0) {
|
|
225
|
-
const block = ensureTextBlock();
|
|
226
|
-
block.text += choice.delta.content;
|
|
227
|
-
stream.push({
|
|
228
|
-
type: "text_delta",
|
|
229
|
-
contentIndex: getContentIndex(block),
|
|
230
|
-
delta: choice.delta.content,
|
|
231
|
-
partial: output,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
// Some endpoints return reasoning in reasoning_content (llama.cpp),
|
|
235
|
-
// or reasoning (other openai compatible endpoints)
|
|
236
|
-
// Use the first non-empty reasoning field to avoid duplication
|
|
237
|
-
// (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
|
|
238
|
-
const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
|
|
239
|
-
const deltaFields = choice.delta;
|
|
240
|
-
let foundReasoningField = null;
|
|
241
|
-
for (const field of reasoningFields) {
|
|
242
|
-
const value = deltaFields[field];
|
|
243
|
-
if (typeof value === "string" && value.length > 0) {
|
|
244
|
-
foundReasoningField = field;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (foundReasoningField) {
|
|
249
|
-
const delta = deltaFields[foundReasoningField];
|
|
250
|
-
if (typeof delta === "string" && delta.length > 0) {
|
|
251
|
-
const thinkingSignature = model.provider === "opencode-go" && foundReasoningField === "reasoning"
|
|
252
|
-
? "reasoning_content"
|
|
253
|
-
: foundReasoningField;
|
|
254
|
-
const block = ensureThinkingBlock(thinkingSignature);
|
|
255
|
-
block.thinking += delta;
|
|
256
|
-
stream.push({
|
|
257
|
-
type: "thinking_delta",
|
|
258
|
-
contentIndex: getContentIndex(block),
|
|
259
|
-
delta,
|
|
260
|
-
partial: output,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (choice?.delta?.tool_calls) {
|
|
265
|
-
for (const toolCall of choice.delta.tool_calls) {
|
|
266
|
-
const block = ensureToolCallBlock(toolCall);
|
|
267
|
-
if (!block.id && toolCall.id) {
|
|
268
|
-
block.id = toolCall.id;
|
|
269
|
-
toolCallBlocksById.set(toolCall.id, block);
|
|
270
|
-
}
|
|
271
|
-
if (!block.name && toolCall.function?.name) {
|
|
272
|
-
block.name = toolCall.function.name;
|
|
273
|
-
}
|
|
274
|
-
let delta = "";
|
|
275
|
-
if (toolCall.function?.arguments) {
|
|
276
|
-
delta = toolCall.function.arguments;
|
|
277
|
-
block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
|
|
278
|
-
block.arguments = parseStreamingJson(block.partialArgs);
|
|
279
|
-
}
|
|
280
|
-
stream.push({
|
|
281
|
-
type: "toolcall_delta",
|
|
282
|
-
contentIndex: getContentIndex(block),
|
|
283
|
-
delta,
|
|
284
|
-
partial: output,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const reasoningDetails = choice.delta.reasoning_details;
|
|
289
|
-
if (reasoningDetails && Array.isArray(reasoningDetails)) {
|
|
290
|
-
for (const detail of reasoningDetails) {
|
|
291
|
-
if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
|
|
292
|
-
const matchingToolCall = output.content.find((b) => b.type === "toolCall" && b.id === detail.id);
|
|
293
|
-
if (matchingToolCall) {
|
|
294
|
-
matchingToolCall.thoughtSignature = JSON.stringify(detail);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
for (const block of blocks) {
|
|
302
|
-
finishBlock(block);
|
|
303
|
-
}
|
|
304
|
-
if (options?.signal?.aborted) {
|
|
305
|
-
throw new Error("Request was aborted");
|
|
306
|
-
}
|
|
307
|
-
if (output.stopReason === "aborted") {
|
|
308
|
-
throw new Error("Request was aborted");
|
|
309
|
-
}
|
|
310
|
-
if (output.stopReason === "error") {
|
|
311
|
-
throw new Error(output.errorMessage || "Provider returned an error stop reason");
|
|
312
|
-
}
|
|
313
|
-
if (!hasFinishReason) {
|
|
314
|
-
throw new Error("Stream ended without finish_reason");
|
|
315
|
-
}
|
|
316
|
-
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
317
|
-
stream.end();
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
for (const block of output.content) {
|
|
321
|
-
delete block.index;
|
|
322
|
-
// Streaming scratch buffers are only used during parsing; never persist them.
|
|
323
|
-
delete block.partialArgs;
|
|
324
|
-
delete block.streamIndex;
|
|
325
|
-
}
|
|
326
|
-
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
327
|
-
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
|
328
|
-
// Some providers via OpenRouter give additional information in this field.
|
|
329
|
-
const rawMetadata = error?.error?.metadata?.raw;
|
|
330
|
-
if (typeof rawMetadata === "string" && rawMetadata.length > 0)
|
|
331
|
-
output.errorMessage += `\n${rawMetadata}`;
|
|
332
|
-
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
333
|
-
stream.end();
|
|
334
|
-
}
|
|
335
|
-
})();
|
|
335
|
+
void runOpenAICompletionsStream(model, context, options, stream);
|
|
336
336
|
return stream;
|
|
337
337
|
};
|
|
338
338
|
export const streamSimpleOpenAICompletions = (model, context, options) => {
|
|
@@ -371,151 +371,168 @@ function createClient(model, context, apiKey, optionsHeaders, sessionId, compat
|
|
|
371
371
|
headers["x-client-request-id"] = sessionId;
|
|
372
372
|
headers["x-session-affinity"] = sessionId;
|
|
373
373
|
}
|
|
374
|
-
|
|
374
|
+
if (model.provider === "cloudflare-ai-gateway") {
|
|
375
|
+
if (!("Authorization" in headers))
|
|
376
|
+
headers.Authorization = null;
|
|
377
|
+
if (!("cf-aig-authorization" in headers))
|
|
378
|
+
headers["cf-aig-authorization"] = `Bearer ${apiKey}`;
|
|
379
|
+
}
|
|
380
|
+
// Merge options headers last so they can override or suppress defaults.
|
|
375
381
|
if (optionsHeaders) {
|
|
376
382
|
Object.assign(headers, optionsHeaders);
|
|
377
383
|
}
|
|
378
|
-
const defaultHeaders = model.provider === "cloudflare-ai-gateway"
|
|
379
|
-
? {
|
|
380
|
-
...headers,
|
|
381
|
-
Authorization: headers.Authorization ?? null,
|
|
382
|
-
"cf-aig-authorization": `Bearer ${apiKey}`,
|
|
383
|
-
}
|
|
384
|
-
: headers;
|
|
385
384
|
return new OpenAI({
|
|
386
385
|
apiKey,
|
|
387
386
|
baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl,
|
|
388
387
|
dangerouslyAllowBrowser: true,
|
|
389
|
-
defaultHeaders,
|
|
388
|
+
defaultHeaders: headers,
|
|
390
389
|
});
|
|
391
390
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
const cacheControl = getCompatCacheControl(compat, cacheRetention);
|
|
395
|
-
const params = {
|
|
396
|
-
model: model.id,
|
|
397
|
-
messages,
|
|
398
|
-
stream: true,
|
|
399
|
-
prompt_cache_key: (model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
|
|
400
|
-
(cacheRetention === "long" && compat.supportsLongCacheRetention)
|
|
401
|
-
? clampOpenAIPromptCacheKey(options?.sessionId)
|
|
402
|
-
: undefined,
|
|
403
|
-
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
|
|
404
|
-
};
|
|
405
|
-
if (compat.supportsUsageInStreaming !== false) {
|
|
391
|
+
function configureOpenAIRequestFields(params, options, compat) {
|
|
392
|
+
if (compat.supportsUsageInStreaming !== false)
|
|
406
393
|
params.stream_options = { include_usage: true };
|
|
407
|
-
|
|
408
|
-
if (compat.supportsStore) {
|
|
394
|
+
if (compat.supportsStore)
|
|
409
395
|
params.store = false;
|
|
410
|
-
}
|
|
411
396
|
if (options?.maxTokens) {
|
|
412
|
-
if (compat.maxTokensField === "max_tokens")
|
|
397
|
+
if (compat.maxTokensField === "max_tokens")
|
|
413
398
|
params.max_tokens = options.maxTokens;
|
|
414
|
-
|
|
415
|
-
else {
|
|
399
|
+
else
|
|
416
400
|
params.max_completion_tokens = options.maxTokens;
|
|
417
|
-
}
|
|
418
401
|
}
|
|
419
|
-
if (options?.temperature !== undefined)
|
|
402
|
+
if (options?.temperature !== undefined)
|
|
420
403
|
params.temperature = options.temperature;
|
|
421
|
-
|
|
404
|
+
}
|
|
405
|
+
function configureOpenAIRequestTools(params, context, compat, cacheControl) {
|
|
422
406
|
if (context.tools && context.tools.length > 0) {
|
|
423
407
|
params.tools = convertTools(context.tools, compat);
|
|
424
|
-
if (compat.zaiToolStream)
|
|
408
|
+
if (compat.zaiToolStream)
|
|
425
409
|
params.tool_stream = true;
|
|
426
|
-
}
|
|
427
410
|
}
|
|
428
411
|
else if (hasToolHistory(context.messages)) {
|
|
429
|
-
// Anthropic
|
|
412
|
+
// Anthropic-compatible proxies require tools when conversation history contains tool calls or results.
|
|
430
413
|
params.tools = [];
|
|
431
414
|
}
|
|
432
|
-
if (cacheControl)
|
|
433
|
-
applyAnthropicCacheControl(messages, params.tools, cacheControl);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
415
|
+
if (cacheControl)
|
|
416
|
+
applyAnthropicCacheControl(params.messages, params.tools, cacheControl);
|
|
417
|
+
}
|
|
418
|
+
function resolveOpenAIReasoningEffort(model, effort) {
|
|
419
|
+
return model.thinkingLevelMap?.[effort] ?? effort;
|
|
420
|
+
}
|
|
421
|
+
function configureZaiReasoning(params, model, options, compat) {
|
|
422
|
+
params.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
|
|
423
|
+
if (!options?.reasoningEffort || !compat.supportsReasoningEffort)
|
|
424
|
+
return;
|
|
425
|
+
const mappedEffort = model.thinkingLevelMap?.[options.reasoningEffort];
|
|
426
|
+
const effort = mappedEffort === undefined ? options.reasoningEffort : mappedEffort;
|
|
427
|
+
if (typeof effort === "string")
|
|
428
|
+
params.reasoning_effort = effort;
|
|
429
|
+
}
|
|
430
|
+
function configureDeepSeekReasoning(params, model, options, compat) {
|
|
431
|
+
params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
|
|
432
|
+
if (!options?.reasoningEffort || !compat.supportsReasoningEffort)
|
|
433
|
+
return;
|
|
434
|
+
params.reasoning_effort = resolveOpenAIReasoningEffort(model, options.reasoningEffort);
|
|
435
|
+
}
|
|
436
|
+
function configureOpenRouterReasoning(params, model, options) {
|
|
437
|
+
if (options?.reasoningEffort) {
|
|
438
|
+
params.reasoning = { effort: resolveOpenAIReasoningEffort(model, options.reasoningEffort) };
|
|
437
439
|
}
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
|
|
441
|
-
if (options?.reasoningEffort && compat.supportsReasoningEffort) {
|
|
442
|
-
const mappedEffort = model.thinkingLevelMap?.[options.reasoningEffort];
|
|
443
|
-
const effort = mappedEffort === undefined ? options.reasoningEffort : mappedEffort;
|
|
444
|
-
if (typeof effort === "string") {
|
|
445
|
-
zaiParams.reasoning_effort = effort;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
440
|
+
else if (model.thinkingLevelMap?.off !== null) {
|
|
441
|
+
params.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
|
448
442
|
}
|
|
449
|
-
|
|
450
|
-
|
|
443
|
+
}
|
|
444
|
+
function configureTogetherReasoning(params, model, options, compat) {
|
|
445
|
+
params.reasoning = { enabled: !!options?.reasoningEffort };
|
|
446
|
+
if (!options?.reasoningEffort || !compat.supportsReasoningEffort)
|
|
447
|
+
return;
|
|
448
|
+
params.reasoning_effort = resolveOpenAIReasoningEffort(model, options.reasoningEffort);
|
|
449
|
+
}
|
|
450
|
+
function configureStringThinking(params, model, options) {
|
|
451
|
+
if (options?.reasoningEffort) {
|
|
452
|
+
params.thinking = resolveOpenAIReasoningEffort(model, options.reasoningEffort);
|
|
451
453
|
}
|
|
452
|
-
else if (
|
|
453
|
-
params.
|
|
454
|
-
enable_thinking: !!options?.reasoningEffort,
|
|
455
|
-
preserve_thinking: true,
|
|
456
|
-
};
|
|
454
|
+
else if (model.thinkingLevelMap?.off !== null) {
|
|
455
|
+
params.thinking = model.thinkingLevelMap?.off ?? "none";
|
|
457
456
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
457
|
+
}
|
|
458
|
+
function configureStandardOpenAIReasoning(params, model, options, compat) {
|
|
459
|
+
if (!compat.supportsReasoningEffort)
|
|
460
|
+
return;
|
|
461
|
+
if (options?.reasoningEffort) {
|
|
462
|
+
params.reasoning_effort = resolveOpenAIReasoningEffort(model, options.reasoningEffort);
|
|
463
|
+
return;
|
|
464
464
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
465
|
+
const offValue = model.thinkingLevelMap?.off;
|
|
466
|
+
if (typeof offValue === "string")
|
|
467
|
+
params.reasoning_effort = offValue;
|
|
468
|
+
}
|
|
469
|
+
function configureOpenAIReasoning(params, model, options, compat) {
|
|
470
|
+
if (!model.reasoning)
|
|
471
|
+
return;
|
|
472
|
+
switch (compat.thinkingFormat) {
|
|
473
|
+
case "zai":
|
|
474
|
+
configureZaiReasoning(params, model, options, compat);
|
|
475
|
+
return;
|
|
476
|
+
case "qwen":
|
|
477
|
+
params.enable_thinking = !!options?.reasoningEffort;
|
|
478
|
+
return;
|
|
479
|
+
case "qwen-chat-template":
|
|
480
|
+
params.chat_template_kwargs = {
|
|
481
|
+
enable_thinking: !!options?.reasoningEffort,
|
|
482
|
+
preserve_thinking: true,
|
|
471
483
|
};
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
stringThinkingParams.thinking = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
|
488
|
-
}
|
|
489
|
-
else if (model.thinkingLevelMap?.off !== null) {
|
|
490
|
-
stringThinkingParams.thinking = model.thinkingLevelMap?.off ?? "none";
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
|
|
494
|
-
// OpenAI-style reasoning_effort
|
|
495
|
-
params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
|
496
|
-
}
|
|
497
|
-
else if (!options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
|
|
498
|
-
const offValue = model.thinkingLevelMap?.off;
|
|
499
|
-
if (typeof offValue === "string") {
|
|
500
|
-
params.reasoning_effort = offValue;
|
|
501
|
-
}
|
|
484
|
+
return;
|
|
485
|
+
case "deepseek":
|
|
486
|
+
configureDeepSeekReasoning(params, model, options, compat);
|
|
487
|
+
return;
|
|
488
|
+
case "openrouter":
|
|
489
|
+
configureOpenRouterReasoning(params, model, options);
|
|
490
|
+
return;
|
|
491
|
+
case "together":
|
|
492
|
+
configureTogetherReasoning(params, model, options, compat);
|
|
493
|
+
return;
|
|
494
|
+
case "string-thinking":
|
|
495
|
+
configureStringThinking(params, model, options);
|
|
496
|
+
return;
|
|
497
|
+
default:
|
|
498
|
+
configureStandardOpenAIReasoning(params, model, options, compat);
|
|
502
499
|
}
|
|
503
|
-
|
|
500
|
+
}
|
|
501
|
+
function configureOpenAIProviderRouting(params, model) {
|
|
504
502
|
if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
|
|
505
503
|
params.provider = model.compat.openRouterRouting;
|
|
506
504
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
505
|
+
if (!model.baseUrl.includes("ai-gateway.vercel.sh") || !model.compat?.vercelGatewayRouting)
|
|
506
|
+
return;
|
|
507
|
+
const routing = model.compat.vercelGatewayRouting;
|
|
508
|
+
if (!routing.only && !routing.order)
|
|
509
|
+
return;
|
|
510
|
+
const gatewayOptions = {};
|
|
511
|
+
if (routing.only)
|
|
512
|
+
gatewayOptions.only = routing.only;
|
|
513
|
+
if (routing.order)
|
|
514
|
+
gatewayOptions.order = routing.order;
|
|
515
|
+
params.providerOptions = { gateway: gatewayOptions };
|
|
516
|
+
}
|
|
517
|
+
function buildParams(model, context, options, compat = getCompat(model), cacheRetention = resolveCacheRetention(options?.cacheRetention)) {
|
|
518
|
+
const messages = convertMessages(model, context, compat);
|
|
519
|
+
const cacheControl = getCompatCacheControl(compat, cacheRetention);
|
|
520
|
+
const params = {
|
|
521
|
+
model: model.id,
|
|
522
|
+
messages,
|
|
523
|
+
stream: true,
|
|
524
|
+
prompt_cache_key: (model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
|
|
525
|
+
(cacheRetention === "long" && compat.supportsLongCacheRetention)
|
|
526
|
+
? clampOpenAIPromptCacheKey(options?.sessionId)
|
|
527
|
+
: undefined,
|
|
528
|
+
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
|
|
529
|
+
};
|
|
530
|
+
configureOpenAIRequestFields(params, options, compat);
|
|
531
|
+
configureOpenAIRequestTools(params, context, compat, cacheControl);
|
|
532
|
+
if (options?.toolChoice)
|
|
533
|
+
params.tool_choice = options.toolChoice;
|
|
534
|
+
configureOpenAIReasoning(params, model, options, compat);
|
|
535
|
+
configureOpenAIProviderRouting(params, model);
|
|
519
536
|
return params;
|
|
520
537
|
}
|
|
521
538
|
function getCompatCacheControl(compat, cacheRetention) {
|
|
@@ -592,228 +609,191 @@ function addCacheControlToTextContent(message, cacheControl) {
|
|
|
592
609
|
}
|
|
593
610
|
return false;
|
|
594
611
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
612
|
+
function normalizeOpenAICompletionsToolCallId(model, id) {
|
|
613
|
+
if (id.includes("|")) {
|
|
614
|
+
const separatorIndex = id.indexOf("|");
|
|
615
|
+
const callId = id.slice(0, separatorIndex).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
616
|
+
const itemId = id.slice(separatorIndex + 1).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
617
|
+
const combinedId = itemId.length > 0 ? `${callId}_${itemId}` : callId;
|
|
618
|
+
if (combinedId.length <= 40)
|
|
619
|
+
return combinedId;
|
|
620
|
+
const hash = shortHash(id).slice(0, 8);
|
|
621
|
+
const prefix = callId.slice(0, Math.max(1, 40 - hash.length - 1));
|
|
622
|
+
return `${prefix}_${hash}`;
|
|
623
|
+
}
|
|
624
|
+
if (model.provider === "openai")
|
|
625
|
+
return id.length > 40 ? id.slice(0, 40) : id;
|
|
626
|
+
return id;
|
|
627
|
+
}
|
|
628
|
+
function convertOpenAIUserMessage(message) {
|
|
629
|
+
if (typeof message.content === "string") {
|
|
630
|
+
return { role: "user", content: sanitizeSurrogates(message.content) };
|
|
631
|
+
}
|
|
632
|
+
const content = message.content.map((item) => {
|
|
633
|
+
if (item.type === "text") {
|
|
634
|
+
return { type: "text", text: sanitizeSurrogates(item.text) };
|
|
606
635
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
636
|
+
return {
|
|
637
|
+
type: "image_url",
|
|
638
|
+
image_url: { url: `data:${item.mimeType};base64,${item.data}` },
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
return content.length > 0 ? { role: "user", content } : undefined;
|
|
642
|
+
}
|
|
643
|
+
function createOpenAIAssistantConversion(message, model, compat) {
|
|
644
|
+
const textParts = message.content
|
|
645
|
+
.filter((block) => isTextContentBlock(block) && block.text.trim().length > 0)
|
|
646
|
+
.map((block) => ({ type: "text", text: sanitizeSurrogates(block.text) }));
|
|
647
|
+
return {
|
|
648
|
+
model,
|
|
649
|
+
compat,
|
|
650
|
+
source: message,
|
|
651
|
+
output: { role: "assistant", content: compat.requiresAssistantAfterToolResult ? "" : null },
|
|
652
|
+
textParts,
|
|
653
|
+
text: textParts.map((part) => part.text).join(""),
|
|
610
654
|
};
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
url: `data:${item.mimeType};base64,${item.data}`,
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
if (content.length === 0)
|
|
653
|
-
continue;
|
|
654
|
-
params.push({
|
|
655
|
-
role: "user",
|
|
656
|
-
content,
|
|
657
|
-
});
|
|
658
|
-
}
|
|
655
|
+
}
|
|
656
|
+
function applyOpenAIAssistantThinking(conversion) {
|
|
657
|
+
const thinkingBlocks = conversion.source.content.filter((block) => isThinkingContentBlock(block) && block.thinking.trim().length > 0);
|
|
658
|
+
if (thinkingBlocks.length === 0) {
|
|
659
|
+
if (conversion.text.length > 0)
|
|
660
|
+
conversion.output.content = conversion.text;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (conversion.compat.requiresThinkingAsText) {
|
|
664
|
+
const thinkingText = thinkingBlocks.map((block) => sanitizeSurrogates(block.thinking)).join("\n\n");
|
|
665
|
+
conversion.output.content = [{ type: "text", text: thinkingText }, ...conversion.textParts];
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (conversion.text.length > 0)
|
|
669
|
+
conversion.output.content = conversion.text;
|
|
670
|
+
let signature = thinkingBlocks[0].thinkingSignature;
|
|
671
|
+
if (conversion.model.provider === "opencode-go" && signature === "reasoning")
|
|
672
|
+
signature = "reasoning_content";
|
|
673
|
+
if (signature && signature.length > 0) {
|
|
674
|
+
conversion.output[signature] = thinkingBlocks.map((block) => block.thinking).join("\n");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function applyOpenAIAssistantToolCalls(conversion) {
|
|
678
|
+
const toolCalls = conversion.source.content.filter(isToolCallBlock);
|
|
679
|
+
if (toolCalls.length === 0)
|
|
680
|
+
return;
|
|
681
|
+
conversion.output.tool_calls = toolCalls.map((toolCall) => ({
|
|
682
|
+
id: toolCall.id,
|
|
683
|
+
type: "function",
|
|
684
|
+
function: { name: toolCall.name, arguments: JSON.stringify(toolCall.arguments) },
|
|
685
|
+
}));
|
|
686
|
+
const reasoningDetails = toolCalls
|
|
687
|
+
.filter((toolCall) => toolCall.thoughtSignature)
|
|
688
|
+
.map((toolCall) => {
|
|
689
|
+
try {
|
|
690
|
+
return JSON.parse(toolCall.thoughtSignature);
|
|
659
691
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const assistantMsg = {
|
|
663
|
-
role: "assistant",
|
|
664
|
-
content: compat.requiresAssistantAfterToolResult ? "" : null,
|
|
665
|
-
};
|
|
666
|
-
const assistantTextParts = msg.content
|
|
667
|
-
.filter(isTextContentBlock)
|
|
668
|
-
.filter((block) => block.text.trim().length > 0)
|
|
669
|
-
.map((block) => ({
|
|
670
|
-
type: "text",
|
|
671
|
-
text: sanitizeSurrogates(block.text),
|
|
672
|
-
}));
|
|
673
|
-
const assistantText = assistantTextParts.map((part) => part.text).join("");
|
|
674
|
-
const nonEmptyThinkingBlocks = msg.content
|
|
675
|
-
.filter(isThinkingContentBlock)
|
|
676
|
-
.filter((block) => block.thinking.trim().length > 0);
|
|
677
|
-
if (nonEmptyThinkingBlocks.length > 0) {
|
|
678
|
-
if (compat.requiresThinkingAsText) {
|
|
679
|
-
// Convert thinking blocks to plain text (no tags to avoid model mimicking them)
|
|
680
|
-
const thinkingText = nonEmptyThinkingBlocks
|
|
681
|
-
.map((block) => sanitizeSurrogates(block.thinking))
|
|
682
|
-
.join("\n\n");
|
|
683
|
-
assistantMsg.content = [{ type: "text", text: thinkingText }, ...assistantTextParts];
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
// Always send assistant content as a plain string (OpenAI Chat Completions
|
|
687
|
-
// API standard format). Sending as an array of {type:"text", text:"..."}
|
|
688
|
-
// objects is non-standard and causes some models (e.g. DeepSeek V3.2 via
|
|
689
|
-
// NVIDIA NIM) to mirror the content-block structure literally in their
|
|
690
|
-
// output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
|
|
691
|
-
if (assistantText.length > 0) {
|
|
692
|
-
assistantMsg.content = assistantText;
|
|
693
|
-
}
|
|
694
|
-
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
|
|
695
|
-
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
|
696
|
-
if (model.provider === "opencode-go" && signature === "reasoning") {
|
|
697
|
-
signature = "reasoning_content";
|
|
698
|
-
}
|
|
699
|
-
if (signature && signature.length > 0) {
|
|
700
|
-
assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
else if (assistantText.length > 0) {
|
|
705
|
-
// Always send assistant content as a plain string (OpenAI Chat Completions
|
|
706
|
-
// API standard format). Sending as an array of {type:"text", text:"..."}
|
|
707
|
-
// objects is non-standard and causes some models (e.g. DeepSeek V3.2 via
|
|
708
|
-
// NVIDIA NIM) to mirror the content-block structure literally in their
|
|
709
|
-
// output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
|
|
710
|
-
assistantMsg.content = assistantText;
|
|
711
|
-
}
|
|
712
|
-
const toolCalls = msg.content.filter(isToolCallBlock);
|
|
713
|
-
if (toolCalls.length > 0) {
|
|
714
|
-
assistantMsg.tool_calls = toolCalls.map((tc) => ({
|
|
715
|
-
id: tc.id,
|
|
716
|
-
type: "function",
|
|
717
|
-
function: {
|
|
718
|
-
name: tc.name,
|
|
719
|
-
arguments: JSON.stringify(tc.arguments),
|
|
720
|
-
},
|
|
721
|
-
}));
|
|
722
|
-
const reasoningDetails = toolCalls
|
|
723
|
-
.filter((tc) => tc.thoughtSignature)
|
|
724
|
-
.map((tc) => {
|
|
725
|
-
try {
|
|
726
|
-
return JSON.parse(tc.thoughtSignature);
|
|
727
|
-
}
|
|
728
|
-
catch {
|
|
729
|
-
return null;
|
|
730
|
-
}
|
|
731
|
-
})
|
|
732
|
-
.filter(Boolean);
|
|
733
|
-
if (reasoningDetails.length > 0) {
|
|
734
|
-
assistantMsg.reasoning_details = reasoningDetails;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
if (compat.requiresReasoningContentOnAssistantMessages &&
|
|
738
|
-
model.reasoning &&
|
|
739
|
-
assistantMsg.reasoning_content === undefined) {
|
|
740
|
-
assistantMsg.reasoning_content = "";
|
|
741
|
-
}
|
|
742
|
-
// Skip assistant messages that have no content and no tool calls.
|
|
743
|
-
// Some providers require "either content or tool_calls, but not none".
|
|
744
|
-
// Other providers also don't accept empty assistant messages.
|
|
745
|
-
// This handles aborted assistant responses that got no content.
|
|
746
|
-
const content = assistantMsg.content;
|
|
747
|
-
const hasContent = content !== null &&
|
|
748
|
-
content !== undefined &&
|
|
749
|
-
(typeof content === "string" ? content.length > 0 : content.length > 0);
|
|
750
|
-
if (!hasContent && !assistantMsg.tool_calls) {
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
params.push(assistantMsg);
|
|
692
|
+
catch {
|
|
693
|
+
return null;
|
|
754
694
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
695
|
+
})
|
|
696
|
+
.filter(Boolean);
|
|
697
|
+
if (reasoningDetails.length > 0)
|
|
698
|
+
conversion.output.reasoning_details = reasoningDetails;
|
|
699
|
+
}
|
|
700
|
+
function ensureOpenAIAssistantReasoningContent(conversion) {
|
|
701
|
+
if (conversion.compat.requiresReasoningContentOnAssistantMessages &&
|
|
702
|
+
conversion.model.reasoning &&
|
|
703
|
+
conversion.output.reasoning_content === undefined) {
|
|
704
|
+
conversion.output.reasoning_content = "";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function hasOpenAIAssistantOutput(message) {
|
|
708
|
+
const content = message.content;
|
|
709
|
+
const hasContent = content !== null &&
|
|
710
|
+
content !== undefined &&
|
|
711
|
+
(typeof content === "string" ? content.length > 0 : content.length > 0);
|
|
712
|
+
return hasContent || message.tool_calls !== undefined;
|
|
713
|
+
}
|
|
714
|
+
function convertOpenAIAssistantMessage(message, model, compat) {
|
|
715
|
+
const conversion = createOpenAIAssistantConversion(message, model, compat);
|
|
716
|
+
applyOpenAIAssistantThinking(conversion);
|
|
717
|
+
applyOpenAIAssistantToolCalls(conversion);
|
|
718
|
+
ensureOpenAIAssistantReasoningContent(conversion);
|
|
719
|
+
return hasOpenAIAssistantOutput(conversion.output) ? conversion.output : undefined;
|
|
720
|
+
}
|
|
721
|
+
function convertOpenAIToolResultMessage(message, compat) {
|
|
722
|
+
const textResult = message.content
|
|
723
|
+
.filter(isTextContentBlock)
|
|
724
|
+
.map((block) => block.text)
|
|
725
|
+
.join("\n");
|
|
726
|
+
const hasImages = message.content.some((content) => content.type === "image");
|
|
727
|
+
const toolResultText = textResult.length > 0 ? textResult : hasImages ? "(see attached image)" : "(no tool output)";
|
|
728
|
+
const result = {
|
|
729
|
+
role: "tool",
|
|
730
|
+
content: sanitizeSurrogates(toolResultText),
|
|
731
|
+
tool_call_id: message.toolCallId,
|
|
732
|
+
};
|
|
733
|
+
if (compat.requiresToolResultName && message.toolName)
|
|
734
|
+
result.name = message.toolName;
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
function collectOpenAIToolResultImages(message, model) {
|
|
738
|
+
if (!model.input.includes("image"))
|
|
739
|
+
return [];
|
|
740
|
+
return message.content.filter(isImageContentBlock).map((block) => ({
|
|
741
|
+
type: "image_url",
|
|
742
|
+
image_url: { url: `data:${block.mimeType};base64,${block.data}` },
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
function appendOpenAIToolResultBatch(state, startIndex) {
|
|
746
|
+
const imageBlocks = [];
|
|
747
|
+
let index = startIndex;
|
|
748
|
+
for (; index < state.messages.length && state.messages[index].role === "toolResult"; index++) {
|
|
749
|
+
const message = state.messages[index];
|
|
750
|
+
state.params.push(convertOpenAIToolResultMessage(message, state.compat));
|
|
751
|
+
imageBlocks.push(...collectOpenAIToolResultImages(message, state.model));
|
|
752
|
+
}
|
|
753
|
+
if (imageBlocks.length === 0) {
|
|
754
|
+
state.lastRole = "toolResult";
|
|
755
|
+
return index - 1;
|
|
756
|
+
}
|
|
757
|
+
if (state.compat.requiresAssistantAfterToolResult) {
|
|
758
|
+
state.params.push({ role: "assistant", content: "I have processed the tool results." });
|
|
759
|
+
}
|
|
760
|
+
state.params.push({
|
|
761
|
+
role: "user",
|
|
762
|
+
content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...imageBlocks],
|
|
763
|
+
});
|
|
764
|
+
state.lastRole = "user";
|
|
765
|
+
return index - 1;
|
|
766
|
+
}
|
|
767
|
+
function appendAssistantAfterToolResultBridge(state, message) {
|
|
768
|
+
if (state.compat.requiresAssistantAfterToolResult && state.lastRole === "toolResult" && message.role === "user") {
|
|
769
|
+
state.params.push({ role: "assistant", content: "I have processed the tool results." });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function convertOpenAINonToolMessage(message, state) {
|
|
773
|
+
return message.role === "user"
|
|
774
|
+
? convertOpenAIUserMessage(message)
|
|
775
|
+
: convertOpenAIAssistantMessage(message, state.model, state.compat);
|
|
776
|
+
}
|
|
777
|
+
export function convertMessages(model, context, compat) {
|
|
778
|
+
const params = [];
|
|
779
|
+
const transformedMessages = transformMessages(context.messages, model, (id) => normalizeOpenAICompletionsToolCallId(model, id));
|
|
780
|
+
if (context.systemPrompt) {
|
|
781
|
+
const role = model.reasoning && compat.supportsDeveloperRole ? "developer" : "system";
|
|
782
|
+
params.push({ role, content: sanitizeSurrogates(context.systemPrompt) });
|
|
783
|
+
}
|
|
784
|
+
const state = { model, compat, messages: transformedMessages, params, lastRole: null };
|
|
785
|
+
for (let index = 0; index < transformedMessages.length; index++) {
|
|
786
|
+
const message = transformedMessages[index];
|
|
787
|
+
appendAssistantAfterToolResultBridge(state, message);
|
|
788
|
+
if (message.role === "toolResult") {
|
|
789
|
+
index = appendOpenAIToolResultBatch(state, index);
|
|
814
790
|
continue;
|
|
815
791
|
}
|
|
816
|
-
|
|
792
|
+
const converted = convertOpenAINonToolMessage(message, state);
|
|
793
|
+
if (!converted)
|
|
794
|
+
continue;
|
|
795
|
+
params.push(converted);
|
|
796
|
+
state.lastRole = message.role;
|
|
817
797
|
}
|
|
818
798
|
return params;
|
|
819
799
|
}
|
|
@@ -883,87 +863,113 @@ function mapStopReason(reason) {
|
|
|
883
863
|
* Provider takes precedence over URL-based detection since it's explicitly configured.
|
|
884
864
|
* Returns a fully resolved OpenAICompletionsCompat object with all fields set.
|
|
885
865
|
*/
|
|
866
|
+
function matchesOpenAICompatibilityFamily(provider, baseUrl, providers, baseUrlFragments) {
|
|
867
|
+
return providers.includes(provider) || baseUrlFragments.some((fragment) => baseUrl.includes(fragment));
|
|
868
|
+
}
|
|
869
|
+
function detectOpenAIThinkingFormat(isDeepSeek, isZai, isTogether, isOpenRouter) {
|
|
870
|
+
if (isDeepSeek)
|
|
871
|
+
return "deepseek";
|
|
872
|
+
if (isZai)
|
|
873
|
+
return "zai";
|
|
874
|
+
if (isTogether)
|
|
875
|
+
return "together";
|
|
876
|
+
return isOpenRouter ? "openrouter" : "openai";
|
|
877
|
+
}
|
|
886
878
|
function detectCompat(model) {
|
|
887
879
|
const provider = model.provider;
|
|
888
880
|
const baseUrl = model.baseUrl;
|
|
889
|
-
const isZai = provider
|
|
890
|
-
const isTogether = provider
|
|
891
|
-
const isMoonshot = provider
|
|
892
|
-
const isOpenRouter = provider
|
|
893
|
-
const isCloudflareWorkersAI = provider
|
|
894
|
-
const isCloudflareAiGateway = provider
|
|
895
|
-
const isNonStandard = provider
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
881
|
+
const isZai = matchesOpenAICompatibilityFamily(provider, baseUrl, ["zai", "zai-coding-cn"], ["api.z.ai", "open.bigmodel.cn"]);
|
|
882
|
+
const isTogether = matchesOpenAICompatibilityFamily(provider, baseUrl, ["together"], ["api.together.ai", "api.together.xyz"]);
|
|
883
|
+
const isMoonshot = matchesOpenAICompatibilityFamily(provider, baseUrl, ["moonshotai", "moonshotai-cn"], ["api.moonshot."]);
|
|
884
|
+
const isOpenRouter = matchesOpenAICompatibilityFamily(provider, baseUrl, ["openrouter"], ["openrouter.ai"]);
|
|
885
|
+
const isCloudflareWorkersAI = matchesOpenAICompatibilityFamily(provider, baseUrl, ["cloudflare-workers-ai"], ["api.cloudflare.com"]);
|
|
886
|
+
const isCloudflareAiGateway = matchesOpenAICompatibilityFamily(provider, baseUrl, ["cloudflare-ai-gateway"], ["gateway.ai.cloudflare.com"]);
|
|
887
|
+
const isNonStandard = matchesOpenAICompatibilityFamily(provider, baseUrl, [
|
|
888
|
+
"cerebras",
|
|
889
|
+
"xai",
|
|
890
|
+
"together",
|
|
891
|
+
"zai",
|
|
892
|
+
"zai-coding-cn",
|
|
893
|
+
"moonshotai",
|
|
894
|
+
"moonshotai-cn",
|
|
895
|
+
"opencode",
|
|
896
|
+
"cloudflare-workers-ai",
|
|
897
|
+
"cloudflare-ai-gateway",
|
|
898
|
+
], [
|
|
899
|
+
"cerebras.ai",
|
|
900
|
+
"api.x.ai",
|
|
901
|
+
"api.together.ai",
|
|
902
|
+
"api.together.xyz",
|
|
903
|
+
"chutes.ai",
|
|
904
|
+
"deepseek.com",
|
|
905
|
+
"api.z.ai",
|
|
906
|
+
"open.bigmodel.cn",
|
|
907
|
+
"api.moonshot.",
|
|
908
|
+
"opencode.ai",
|
|
909
|
+
"api.cloudflare.com",
|
|
910
|
+
"gateway.ai.cloudflare.com",
|
|
911
|
+
]);
|
|
912
|
+
const isDeepSeek = matchesOpenAICompatibilityFamily(provider, baseUrl, ["deepseek"], ["deepseek.com"]);
|
|
913
|
+
const useMaxTokens = [
|
|
914
|
+
baseUrl.includes("chutes.ai"),
|
|
915
|
+
isDeepSeek,
|
|
916
|
+
isZai,
|
|
917
|
+
isMoonshot,
|
|
918
|
+
isCloudflareAiGateway,
|
|
919
|
+
isTogether,
|
|
920
|
+
].some(Boolean);
|
|
921
|
+
const isGrok = matchesOpenAICompatibilityFamily(provider, baseUrl, ["xai"], ["api.x.ai"]);
|
|
911
922
|
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
|
|
912
923
|
return {
|
|
913
924
|
supportsStore: !isNonStandard,
|
|
914
|
-
supportsDeveloperRole: !isNonStandard
|
|
915
|
-
supportsReasoningEffort: !isGrok
|
|
925
|
+
supportsDeveloperRole: ![isNonStandard, isOpenRouter].some(Boolean),
|
|
926
|
+
supportsReasoningEffort: ![isGrok, isZai, isMoonshot, isTogether, isCloudflareAiGateway].some(Boolean),
|
|
916
927
|
supportsUsageInStreaming: true,
|
|
917
928
|
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
|
918
929
|
requiresToolResultName: false,
|
|
919
930
|
requiresAssistantAfterToolResult: false,
|
|
920
931
|
requiresThinkingAsText: false,
|
|
921
932
|
requiresReasoningContentOnAssistantMessages: isDeepSeek,
|
|
922
|
-
thinkingFormat: isDeepSeek
|
|
923
|
-
? "deepseek"
|
|
924
|
-
: isZai
|
|
925
|
-
? "zai"
|
|
926
|
-
: isTogether
|
|
927
|
-
? "together"
|
|
928
|
-
: isOpenRouter
|
|
929
|
-
? "openrouter"
|
|
930
|
-
: "openai",
|
|
933
|
+
thinkingFormat: detectOpenAIThinkingFormat(isDeepSeek, isZai, isTogether, isOpenRouter),
|
|
931
934
|
openRouterRouting: {},
|
|
932
935
|
vercelGatewayRouting: {},
|
|
933
936
|
zaiToolStream: false,
|
|
934
|
-
supportsStrictMode: !isMoonshot
|
|
937
|
+
supportsStrictMode: ![isMoonshot, isTogether, isCloudflareAiGateway].some(Boolean),
|
|
935
938
|
cacheControlFormat,
|
|
936
939
|
sendSessionAffinityHeaders: false,
|
|
937
|
-
supportsLongCacheRetention: !
|
|
940
|
+
supportsLongCacheRetention: ![isTogether, isCloudflareWorkersAI, isCloudflareAiGateway].some(Boolean),
|
|
938
941
|
};
|
|
939
942
|
}
|
|
943
|
+
function resolveOpenAICompatibilityValue(configuredValue, detectedValue) {
|
|
944
|
+
return configuredValue ?? detectedValue;
|
|
945
|
+
}
|
|
940
946
|
/**
|
|
941
947
|
* Get resolved compatibility settings for a model.
|
|
942
948
|
* Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
|
|
943
949
|
*/
|
|
944
950
|
function getCompat(model) {
|
|
945
951
|
const detected = detectCompat(model);
|
|
946
|
-
|
|
952
|
+
const configured = model.compat;
|
|
953
|
+
if (!configured)
|
|
947
954
|
return detected;
|
|
948
955
|
return {
|
|
949
|
-
supportsStore:
|
|
950
|
-
supportsDeveloperRole:
|
|
951
|
-
supportsReasoningEffort:
|
|
952
|
-
supportsUsageInStreaming:
|
|
953
|
-
maxTokensField:
|
|
954
|
-
requiresToolResultName:
|
|
955
|
-
requiresAssistantAfterToolResult:
|
|
956
|
-
requiresThinkingAsText:
|
|
957
|
-
requiresReasoningContentOnAssistantMessages:
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention,
|
|
956
|
+
supportsStore: resolveOpenAICompatibilityValue(configured.supportsStore, detected.supportsStore),
|
|
957
|
+
supportsDeveloperRole: resolveOpenAICompatibilityValue(configured.supportsDeveloperRole, detected.supportsDeveloperRole),
|
|
958
|
+
supportsReasoningEffort: resolveOpenAICompatibilityValue(configured.supportsReasoningEffort, detected.supportsReasoningEffort),
|
|
959
|
+
supportsUsageInStreaming: resolveOpenAICompatibilityValue(configured.supportsUsageInStreaming, detected.supportsUsageInStreaming),
|
|
960
|
+
maxTokensField: resolveOpenAICompatibilityValue(configured.maxTokensField, detected.maxTokensField),
|
|
961
|
+
requiresToolResultName: resolveOpenAICompatibilityValue(configured.requiresToolResultName, detected.requiresToolResultName),
|
|
962
|
+
requiresAssistantAfterToolResult: resolveOpenAICompatibilityValue(configured.requiresAssistantAfterToolResult, detected.requiresAssistantAfterToolResult),
|
|
963
|
+
requiresThinkingAsText: resolveOpenAICompatibilityValue(configured.requiresThinkingAsText, detected.requiresThinkingAsText),
|
|
964
|
+
requiresReasoningContentOnAssistantMessages: resolveOpenAICompatibilityValue(configured.requiresReasoningContentOnAssistantMessages, detected.requiresReasoningContentOnAssistantMessages),
|
|
965
|
+
thinkingFormat: resolveOpenAICompatibilityValue(configured.thinkingFormat, detected.thinkingFormat),
|
|
966
|
+
openRouterRouting: resolveOpenAICompatibilityValue(configured.openRouterRouting, {}),
|
|
967
|
+
vercelGatewayRouting: resolveOpenAICompatibilityValue(configured.vercelGatewayRouting, detected.vercelGatewayRouting),
|
|
968
|
+
zaiToolStream: resolveOpenAICompatibilityValue(configured.zaiToolStream, detected.zaiToolStream),
|
|
969
|
+
supportsStrictMode: resolveOpenAICompatibilityValue(configured.supportsStrictMode, detected.supportsStrictMode),
|
|
970
|
+
cacheControlFormat: resolveOpenAICompatibilityValue(configured.cacheControlFormat, detected.cacheControlFormat),
|
|
971
|
+
sendSessionAffinityHeaders: resolveOpenAICompatibilityValue(configured.sendSessionAffinityHeaders, detected.sendSessionAffinityHeaders),
|
|
972
|
+
supportsLongCacheRetention: resolveOpenAICompatibilityValue(configured.supportsLongCacheRetention, detected.supportsLongCacheRetention),
|
|
967
973
|
};
|
|
968
974
|
}
|
|
969
975
|
//# sourceMappingURL=openai-completions.js.map
|