@jecruz/merc-ai 0.1.4 → 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/README.md +34 -7
- 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 +345 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +348 -3
- package/dist/image-models.generated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +15282 -3882
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +18368 -10964
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +4 -1
- package/dist/models.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +188 -19
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +101 -34
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/azure-openai-responses.d.ts +2 -0
- package/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.js +8 -3
- package/dist/providers/azure-openai-responses.js.map +1 -1
- package/dist/providers/google-shared.d.ts +15 -4
- package/dist/providers/google-shared.d.ts.map +1 -1
- package/dist/providers/google-shared.js +80 -15
- package/dist/providers/google-shared.js.map +1 -1
- package/dist/providers/google-vertex.d.ts.map +1 -1
- package/dist/providers/google-vertex.js +23 -60
- package/dist/providers/google-vertex.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +19 -68
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/mistral.d.ts +1 -1
- package/dist/providers/mistral.d.ts.map +1 -1
- package/dist/providers/mistral.js +44 -60
- package/dist/providers/mistral.js.map +1 -1
- package/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/dist/providers/openai-codex-responses.js +250 -124
- package/dist/providers/openai-codex-responses.js.map +1 -1
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +94 -46
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-prompt-cache.d.ts +3 -0
- package/dist/providers/openai-prompt-cache.d.ts.map +1 -0
- package/dist/providers/openai-prompt-cache.js +10 -0
- package/dist/providers/openai-prompt-cache.js.map +1 -0
- package/dist/providers/openai-responses-shared.d.ts.map +1 -1
- package/dist/providers/openai-responses-shared.js +154 -211
- package/dist/providers/openai-responses-shared.js.map +1 -1
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +15 -9
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openrouter-images.d.ts.map +1 -1
- package/dist/providers/openrouter-images.js +5 -2
- package/dist/providers/openrouter-images.js.map +1 -1
- package/dist/providers/simple-options.d.ts +15 -3
- package/dist/providers/simple-options.d.ts.map +1 -1
- package/dist/providers/simple-options.js +36 -4
- package/dist/providers/simple-options.js.map +1 -1
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/error-body.d.ts +5 -0
- package/dist/utils/error-body.d.ts.map +1 -1
- package/dist/utils/error-body.js +53 -5
- package/dist/utils/error-body.js.map +1 -1
- package/dist/utils/estimate.d.ts +22 -0
- package/dist/utils/estimate.d.ts.map +1 -0
- package/dist/utils/estimate.js +104 -0
- package/dist/utils/estimate.js.map +1 -0
- package/dist/utils/event-stream.d.ts.map +1 -1
- package/dist/utils/event-stream.js +25 -7
- package/dist/utils/event-stream.js.map +1 -1
- package/dist/utils/json-parse.d.ts +3 -0
- package/dist/utils/json-parse.d.ts.map +1 -1
- package/dist/utils/json-parse.js +15 -0
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/oauth/anthropic.d.ts.map +1 -1
- package/dist/utils/oauth/anthropic.js +16 -10
- package/dist/utils/oauth/anthropic.js.map +1 -1
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/dist/utils/oauth/github-copilot.js +57 -21
- package/dist/utils/oauth/github-copilot.js.map +1 -1
- package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
- package/dist/utils/oauth/openai-codex.js +73 -16
- package/dist/utils/oauth/openai-codex.js.map +1 -1
- package/dist/utils/oauth/types.d.ts +2 -0
- package/dist/utils/oauth/types.d.ts.map +1 -1
- package/dist/utils/oauth/types.js.map +1 -1
- package/dist/utils/overflow.d.ts +10 -1
- package/dist/utils/overflow.d.ts.map +1 -1
- package/dist/utils/overflow.js +47 -7
- package/dist/utils/overflow.js.map +1 -1
- package/dist/utils/provider-env.d.ts.map +1 -1
- package/dist/utils/provider-env.js +45 -10
- package/dist/utils/provider-env.js.map +1 -1
- package/dist/utils/tool-call-id.d.ts +11 -0
- package/dist/utils/tool-call-id.d.ts.map +1 -0
- package/dist/utils/tool-call-id.js +96 -0
- package/dist/utils/tool-call-id.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +6 -3
|
@@ -4,11 +4,13 @@ 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
6
|
import { headersToRecord } from "../utils/headers.js";
|
|
7
|
-
import { parseStreamingJson } from "../utils/json-parse.js";
|
|
7
|
+
import { finalizeToolCallArguments, parseStreamingJson } from "../utils/json-parse.js";
|
|
8
8
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
9
9
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|
10
|
+
import { createProviderToolCallIdNormalizer } from "../utils/tool-call-id.js";
|
|
10
11
|
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
11
12
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
13
|
+
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
12
14
|
import { buildBaseOptions } from "./simple-options.js";
|
|
13
15
|
import { transformMessages } from "./transform-messages.js";
|
|
14
16
|
/**
|
|
@@ -42,9 +44,8 @@ function isImageContentBlock(block) {
|
|
|
42
44
|
return block.type === "image";
|
|
43
45
|
}
|
|
44
46
|
function isMiniMaxModel(model) {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
return model.provider === "llmdynamix" || model.id.toLowerCase().includes("minimax-m3");
|
|
47
|
+
// Merc-native: shared gateways carry multiple families; infer MiniMax only from the id.
|
|
48
|
+
return model.id.toLowerCase().includes("minimax-m3");
|
|
48
49
|
}
|
|
49
50
|
function createThinkingTagFilter(enabled) {
|
|
50
51
|
let buffer = "";
|
|
@@ -102,6 +103,17 @@ function createThinkingTagFilter(enabled) {
|
|
|
102
103
|
flush: () => (enabled ? consume() : buffer),
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
function isEncryptedReasoningDetail(detail) {
|
|
107
|
+
if (typeof detail !== "object" || detail === null) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const candidate = detail;
|
|
111
|
+
return (candidate.type === "reasoning.encrypted" &&
|
|
112
|
+
typeof candidate.id === "string" &&
|
|
113
|
+
candidate.id.length > 0 &&
|
|
114
|
+
typeof candidate.data === "string" &&
|
|
115
|
+
candidate.data.length > 0);
|
|
116
|
+
}
|
|
105
117
|
function isOpenRouterImageGenerationModel(model) {
|
|
106
118
|
return model.provider === "openrouter" && model.compat?.openRouterImageGeneration === true;
|
|
107
119
|
}
|
|
@@ -148,7 +160,7 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
148
160
|
const requestOptions = {
|
|
149
161
|
...(options?.signal ? { signal: options.signal } : {}),
|
|
150
162
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
151
|
-
|
|
163
|
+
maxRetries: options?.maxRetries ?? 0,
|
|
152
164
|
};
|
|
153
165
|
if (isOpenRouterImageGenerationModel(model)) {
|
|
154
166
|
const { stream_options: _streamOptions, ...nonStreamingBaseParams } = params;
|
|
@@ -219,10 +231,12 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
219
231
|
let textBlock = null;
|
|
220
232
|
let thinkingBlock = null;
|
|
221
233
|
let hasFinishReason = false;
|
|
234
|
+
let hasIncompleteToolCallArguments = false;
|
|
222
235
|
const toolCallBlocksByIndex = new Map();
|
|
223
236
|
const toolCallBlocksById = new Map();
|
|
237
|
+
const pendingReasoningDetailsByToolCallId = new Map();
|
|
224
238
|
const blocks = output.content;
|
|
225
|
-
const thinkingTagFilter = createThinkingTagFilter(
|
|
239
|
+
const thinkingTagFilter = createThinkingTagFilter(compat.thinkingFormat === "minimax");
|
|
226
240
|
const getContentIndex = (block) => blocks.indexOf(block);
|
|
227
241
|
const finishBlock = (block) => {
|
|
228
242
|
const contentIndex = getContentIndex(block);
|
|
@@ -246,7 +260,14 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
246
260
|
});
|
|
247
261
|
}
|
|
248
262
|
else if (block.type === "toolCall") {
|
|
249
|
-
|
|
263
|
+
// Without finish_reason, only strictly valid raw arguments prove the call was not cut off.
|
|
264
|
+
try {
|
|
265
|
+
JSON.parse(block.partialArgs ?? "");
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
hasIncompleteToolCallArguments = true;
|
|
269
|
+
}
|
|
270
|
+
finalizeToolCallArguments(block, block.partialArgs);
|
|
250
271
|
// Finalize in-place and strip the scratch buffers so replay only
|
|
251
272
|
// carries parsed arguments.
|
|
252
273
|
delete block.partialArgs;
|
|
@@ -291,6 +312,16 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
291
312
|
}
|
|
292
313
|
return thinkingBlock;
|
|
293
314
|
};
|
|
315
|
+
const applyPendingReasoningDetail = (block) => {
|
|
316
|
+
if (!block.id) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const pendingReasoningDetail = pendingReasoningDetailsByToolCallId.get(block.id);
|
|
320
|
+
if (pendingReasoningDetail) {
|
|
321
|
+
block.thoughtSignature = pendingReasoningDetail;
|
|
322
|
+
pendingReasoningDetailsByToolCallId.delete(block.id);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
294
325
|
const ensureToolCallBlock = (toolCall) => {
|
|
295
326
|
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
296
327
|
let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
|
|
@@ -326,6 +357,7 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
326
357
|
if (toolCall.id) {
|
|
327
358
|
toolCallBlocksById.set(toolCall.id, block);
|
|
328
359
|
}
|
|
360
|
+
applyPendingReasoningDetail(block);
|
|
329
361
|
return block;
|
|
330
362
|
};
|
|
331
363
|
for await (const chunk of openaiStream) {
|
|
@@ -377,8 +409,7 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
377
409
|
}
|
|
378
410
|
}
|
|
379
411
|
if (foundReasoningField &&
|
|
380
|
-
!(
|
|
381
|
-
(!options?.reasoningEffort || model.reasoning === false))) {
|
|
412
|
+
!(compat.thinkingFormat === "minimax" && (!options?.reasoningEffort || model.reasoning === false))) {
|
|
382
413
|
const delta = deltaFields[foundReasoningField];
|
|
383
414
|
if (typeof delta === "string" && delta.length > 0) {
|
|
384
415
|
const thinkingSignature = model.provider === "opencode-go" && foundReasoningField === "reasoning"
|
|
@@ -419,12 +450,16 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
419
450
|
}
|
|
420
451
|
}
|
|
421
452
|
const reasoningDetails = choice.delta.reasoning_details;
|
|
422
|
-
if (
|
|
453
|
+
if (Array.isArray(reasoningDetails)) {
|
|
423
454
|
for (const detail of reasoningDetails) {
|
|
424
|
-
if (detail
|
|
425
|
-
const
|
|
455
|
+
if (isEncryptedReasoningDetail(detail)) {
|
|
456
|
+
const serializedDetail = JSON.stringify(detail);
|
|
457
|
+
const matchingToolCall = toolCallBlocksById.get(detail.id);
|
|
426
458
|
if (matchingToolCall) {
|
|
427
|
-
matchingToolCall.thoughtSignature =
|
|
459
|
+
matchingToolCall.thoughtSignature = serializedDetail;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
pendingReasoningDetailsByToolCallId.set(detail.id, serializedDetail);
|
|
428
463
|
}
|
|
429
464
|
}
|
|
430
465
|
}
|
|
@@ -441,10 +476,16 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
441
476
|
if (output.stopReason === "aborted") {
|
|
442
477
|
throw new Error("Request was aborted");
|
|
443
478
|
}
|
|
479
|
+
if (!hasFinishReason && !compat.supportsFinishReason) {
|
|
480
|
+
if (hasIncompleteToolCallArguments) {
|
|
481
|
+
throw new Error("Stream ended without finish_reason and with incomplete tool call arguments");
|
|
482
|
+
}
|
|
483
|
+
output.stopReason = output.content.some((block) => block.type === "toolCall") ? "toolUse" : "stop";
|
|
484
|
+
}
|
|
444
485
|
if (output.stopReason === "error") {
|
|
445
486
|
throw new Error(output.errorMessage || "Provider returned an error stop reason");
|
|
446
487
|
}
|
|
447
|
-
if (!hasFinishReason) {
|
|
488
|
+
if (compat.supportsFinishReason && !hasFinishReason) {
|
|
448
489
|
throw new Error("Stream ended without finish_reason");
|
|
449
490
|
}
|
|
450
491
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
@@ -458,11 +499,17 @@ export const streamOpenAICompletions = (model, context, options) => {
|
|
|
458
499
|
delete block.streamIndex;
|
|
459
500
|
}
|
|
460
501
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
461
|
-
|
|
502
|
+
const normalized = normalizeProviderError(error);
|
|
503
|
+
output.errorMetadata = normalized.errorMetadata;
|
|
504
|
+
output.errorMessage = formatProviderError(normalized);
|
|
462
505
|
// Some providers via OpenRouter give additional information in this field.
|
|
506
|
+
// normalizeProviderError already stringifies the parsed body (error.error)
|
|
507
|
+
// into errorMessage, so only append the raw metadata when it is not already
|
|
508
|
+
// present to avoid double-printing it.
|
|
463
509
|
const rawMetadata = error?.error?.metadata?.raw;
|
|
464
|
-
if (rawMetadata)
|
|
510
|
+
if (rawMetadata && !output.errorMessage.includes(String(rawMetadata))) {
|
|
465
511
|
output.errorMessage += `\n${rawMetadata}`;
|
|
512
|
+
}
|
|
466
513
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
467
514
|
stream.end();
|
|
468
515
|
}
|
|
@@ -474,7 +521,7 @@ export const streamSimpleOpenAICompletions = (model, context, options) => {
|
|
|
474
521
|
if (!apiKey && model.authHeader !== false) {
|
|
475
522
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
476
523
|
}
|
|
477
|
-
const base = buildBaseOptions(model, options, apiKey ?? "");
|
|
524
|
+
const base = buildBaseOptions(model, context, options, apiKey ?? "");
|
|
478
525
|
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
479
526
|
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
480
527
|
const toolChoice = options?.toolChoice;
|
|
@@ -544,7 +591,7 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
544
591
|
stream: true,
|
|
545
592
|
prompt_cache_key: (model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
|
|
546
593
|
(cacheRetention === "long" && compat.supportsLongCacheRetention)
|
|
547
|
-
? options?.sessionId
|
|
594
|
+
? clampOpenAIPromptCacheKey(options?.sessionId)
|
|
548
595
|
: undefined,
|
|
549
596
|
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
|
|
550
597
|
};
|
|
@@ -581,12 +628,12 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
581
628
|
if (cacheControl) {
|
|
582
629
|
applyAnthropicCacheControl(messages, params.tools, cacheControl);
|
|
583
630
|
}
|
|
584
|
-
if (options?.toolChoice) {
|
|
631
|
+
if (options?.toolChoice && params.tools?.length) {
|
|
585
632
|
params.tool_choice = options.toolChoice;
|
|
586
633
|
}
|
|
587
634
|
if (compat.thinkingFormat === "zai" && model.reasoning) {
|
|
588
635
|
const zaiParams = params;
|
|
589
|
-
zaiParams.thinking =
|
|
636
|
+
zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
|
|
590
637
|
if (options?.reasoningEffort && compat.supportsReasoningEffort) {
|
|
591
638
|
const mappedEffort = model.thinkingLevelMap?.[options.reasoningEffort];
|
|
592
639
|
const effort = mappedEffort === undefined ? options.reasoningEffort : mappedEffort;
|
|
@@ -595,7 +642,7 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
595
642
|
}
|
|
596
643
|
}
|
|
597
644
|
}
|
|
598
|
-
else if (compat.thinkingFormat === "minimax"
|
|
645
|
+
else if (compat.thinkingFormat === "minimax") {
|
|
599
646
|
const minimaxParams = params;
|
|
600
647
|
minimaxParams.thinking = { type: options?.reasoningEffort ? "adaptive" : "disabled" };
|
|
601
648
|
minimaxParams.reasoning_split = true;
|
|
@@ -690,7 +737,7 @@ function addCacheControlToSystemPrompt(messages, cacheControl) {
|
|
|
690
737
|
function addCacheControlToLastConversationMessage(messages, cacheControl) {
|
|
691
738
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
692
739
|
const message = messages[i];
|
|
693
|
-
if (message.role === "user" || message.role === "assistant") {
|
|
740
|
+
if (message.role === "user" || message.role === "assistant" || message.role === "tool") {
|
|
694
741
|
if (addCacheControlToMessage(message, cacheControl)) {
|
|
695
742
|
return;
|
|
696
743
|
}
|
|
@@ -708,7 +755,7 @@ function addCacheControlToInstructionMessage(message, cacheControl) {
|
|
|
708
755
|
return addCacheControlToTextContent(message, cacheControl);
|
|
709
756
|
}
|
|
710
757
|
function addCacheControlToMessage(message, cacheControl) {
|
|
711
|
-
if (message.role === "user" || message.role === "assistant") {
|
|
758
|
+
if (message.role === "user" || message.role === "assistant" || message.role === "tool") {
|
|
712
759
|
return addCacheControlToTextContent(message, cacheControl);
|
|
713
760
|
}
|
|
714
761
|
return false;
|
|
@@ -743,20 +790,7 @@ function addCacheControlToTextContent(message, cacheControl) {
|
|
|
743
790
|
}
|
|
744
791
|
export function convertMessages(model, context, compat) {
|
|
745
792
|
const params = [];
|
|
746
|
-
const normalizeToolCallId = (
|
|
747
|
-
// Handle pipe-separated IDs from OpenAI Responses API
|
|
748
|
-
// Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
|
|
749
|
-
// These come from providers like github-copilot, openai-codex, opencode
|
|
750
|
-
// Extract just the call_id part and normalize it
|
|
751
|
-
if (id.includes("|")) {
|
|
752
|
-
const [callId] = id.split("|");
|
|
753
|
-
// Sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
|
|
754
|
-
return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
755
|
-
}
|
|
756
|
-
if (model.provider === "openai")
|
|
757
|
-
return id.length > 40 ? id.slice(0, 40) : id;
|
|
758
|
-
return id;
|
|
759
|
-
};
|
|
793
|
+
const normalizeToolCallId = createProviderToolCallIdNormalizer(model);
|
|
760
794
|
const transformedMessages = transformMessages(context.messages, model, (id) => normalizeToolCallId(id));
|
|
761
795
|
if (context.systemPrompt) {
|
|
762
796
|
const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
|
|
@@ -782,7 +816,9 @@ export function convertMessages(model, context, compat) {
|
|
|
782
816
|
});
|
|
783
817
|
}
|
|
784
818
|
else {
|
|
785
|
-
const content = msg.content
|
|
819
|
+
const content = msg.content
|
|
820
|
+
.filter((item) => item.type !== "text" || item.text.length > 0)
|
|
821
|
+
.map((item) => {
|
|
786
822
|
if (item.type === "text") {
|
|
787
823
|
return {
|
|
788
824
|
type: "text",
|
|
@@ -981,10 +1017,13 @@ function convertTools(tools, compat) {
|
|
|
981
1017
|
}
|
|
982
1018
|
function parseChunkUsage(rawUsage, model) {
|
|
983
1019
|
const promptTokens = rawUsage.prompt_tokens || 0;
|
|
984
|
-
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
|
|
1020
|
+
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? rawUsage.cached_tokens ?? 0;
|
|
985
1021
|
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
|
|
986
1022
|
// Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
|
|
987
|
-
// tokens (hits).
|
|
1023
|
+
// tokens (hits). Providers disagree on placement: OpenAI/OpenRouter use
|
|
1024
|
+
// prompt_tokens_details.cached_tokens, DeepSeek uses prompt_cache_hit_tokens,
|
|
1025
|
+
// and Kimi documents top-level usage.cached_tokens on the final usage chunk.
|
|
1026
|
+
// OpenAI does not document or emit cache_write_tokens, but
|
|
988
1027
|
// OpenRouter-compatible providers can include it as a separate write count.
|
|
989
1028
|
// OpenRouter's own provider/tests affirm the separate mapping:
|
|
990
1029
|
// https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409
|
|
@@ -1042,12 +1081,13 @@ function detectCompat(model) {
|
|
|
1042
1081
|
const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
|
|
1043
1082
|
const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com");
|
|
1044
1083
|
const isMiniMax = isMiniMaxModel(model);
|
|
1084
|
+
const isDeepSeek = provider === "deepseek" || baseUrl.toLowerCase().includes("deepseek.com");
|
|
1045
1085
|
const isNonStandard = provider === "cerebras" ||
|
|
1046
1086
|
baseUrl.includes("cerebras.ai") ||
|
|
1047
1087
|
provider === "xai" ||
|
|
1048
1088
|
baseUrl.includes("api.x.ai") ||
|
|
1049
1089
|
baseUrl.includes("chutes.ai") ||
|
|
1050
|
-
|
|
1090
|
+
isDeepSeek ||
|
|
1051
1091
|
isZai ||
|
|
1052
1092
|
isMoonshot ||
|
|
1053
1093
|
provider === "opencode" ||
|
|
@@ -1055,16 +1095,23 @@ function detectCompat(model) {
|
|
|
1055
1095
|
isCloudflareWorkersAI ||
|
|
1056
1096
|
isCloudflareAiGateway ||
|
|
1057
1097
|
isAntLing;
|
|
1058
|
-
|
|
1098
|
+
// Z.AI ignores max_completion_tokens, so its output cap only applies through max_tokens.
|
|
1099
|
+
const isZaiMaxTokensEndpoint = isZai || provider === "zai-coding-cn" || baseUrl.includes("open.bigmodel.cn");
|
|
1100
|
+
const useMaxTokens = baseUrl.includes("chutes.ai") ||
|
|
1101
|
+
isDeepSeek ||
|
|
1102
|
+
isMoonshot ||
|
|
1103
|
+
isCloudflareAiGateway ||
|
|
1104
|
+
isAntLing ||
|
|
1105
|
+
isZaiMaxTokensEndpoint;
|
|
1059
1106
|
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
|
1060
|
-
const
|
|
1061
|
-
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
|
|
1107
|
+
const cacheControlFormat = provider === "openrouter" && /^~?anthropic\//.test(model.id) ? "anthropic" : undefined;
|
|
1062
1108
|
const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai");
|
|
1063
1109
|
return {
|
|
1064
1110
|
supportsStore: !isNonStandard,
|
|
1065
1111
|
supportsDeveloperRole: !isNonStandard,
|
|
1066
1112
|
supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isCloudflareAiGateway && !isAntLing,
|
|
1067
1113
|
supportsUsageInStreaming: true,
|
|
1114
|
+
supportsFinishReason: true,
|
|
1068
1115
|
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
|
1069
1116
|
requiresToolResultName: false,
|
|
1070
1117
|
requiresAssistantAfterToolResult: false,
|
|
@@ -1087,7 +1134,7 @@ function detectCompat(model) {
|
|
|
1087
1134
|
zaiToolStream: false,
|
|
1088
1135
|
supportsStrictMode: !isMoonshot && !isCloudflareAiGateway,
|
|
1089
1136
|
cacheControlFormat,
|
|
1090
|
-
sendSessionAffinityHeaders:
|
|
1137
|
+
sendSessionAffinityHeaders: isOpenRouter,
|
|
1091
1138
|
sessionAffinityFormat: isOpenRouter ? "openrouter" : "openai",
|
|
1092
1139
|
supportsLongCacheRetention: !(isCloudflareWorkersAI || isCloudflareAiGateway || isAntLing),
|
|
1093
1140
|
};
|
|
@@ -1105,13 +1152,14 @@ function getCompat(model) {
|
|
|
1105
1152
|
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
|
|
1106
1153
|
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
|
|
1107
1154
|
supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
|
|
1155
|
+
supportsFinishReason: model.compat.supportsFinishReason ?? detected.supportsFinishReason,
|
|
1108
1156
|
maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
|
|
1109
1157
|
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
|
|
1110
1158
|
requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
|
|
1111
1159
|
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
|
|
1112
1160
|
requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ??
|
|
1113
1161
|
detected.requiresReasoningContentOnAssistantMessages,
|
|
1114
|
-
thinkingFormat:
|
|
1162
|
+
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
|
1115
1163
|
openRouterRouting: model.compat.openRouterRouting ?? {},
|
|
1116
1164
|
openRouterImageGeneration: model.compat.openRouterImageGeneration ?? detected.openRouterImageGeneration,
|
|
1117
1165
|
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|