openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,090 lines (1,089 loc) • 43.5 kB
JavaScript
import { t as AssistantMessageEventStream } from "./event-stream-ReMmOTzX.js";
import { l as calculateCost, n as transformMessages, s as buildBaseOptions, t as sanitizeSurrogates, u as clampThinkingLevel } from "./sanitize-unicode-CgzZKr22.js";
import { n as getEnvApiKey } from "./env-api-keys-8q9bEA0v.js";
import { n as parseStreamingJson } from "./json-parse-CydVzlvP.js";
import { a as stripSystemPromptCacheBoundary, i as splitSystemPromptCacheBoundary } from "./system-prompt-cache-boundary-vl0D_wqS.js";
import { i as scanFenceSpans } from "./fences-DSvtxlT8.js";
import { t as headersToRecord } from "./headers-CaXpIDsu.js";
import { i as resolveCloudflareBaseUrl, n as hasCopilotVisionInput, r as isCloudflareProvider, t as buildCopilotDynamicHeaders } from "./github-copilot-headers-zVCJiu45.js";
import { t as clampOpenAIPromptCacheKey } from "./openai-prompt-cache-Bx1f-UHU.js";
import OpenAI from "openai";
//#region packages/markdown-core/src/code-spans.ts
/** Creates the carry-forward state used when scanning inline code across chunks. */
function createInlineCodeState() {
return {
open: false,
ticks: 0
};
}
/** Builds a lookup for fenced and inline code spans while preserving scanner state. */
function buildCodeSpanIndex(text, inlineState, fenceState) {
const { spans: fenceSpans, state: nextFenceState } = scanFenceSpans(text, fenceState);
const { spans: inlineSpans, state: nextInlineState } = parseInlineCodeSpans(text, fenceSpans, inlineState ? {
open: inlineState.open,
ticks: inlineState.ticks
} : createInlineCodeState());
return {
inlineState: nextInlineState,
fenceState: nextFenceState,
isInside: (index) => isInsideFenceSpan(index, fenceSpans) || isInsideInlineSpan(index, inlineSpans)
};
}
function parseInlineCodeSpans(text, fenceSpans, initialState) {
const spans = [];
let open = initialState.open;
let ticks = initialState.ticks;
let openStart = open ? 0 : -1;
let i = 0;
while (i < text.length) {
const fence = findFenceSpanAtInclusive(fenceSpans, i);
if (fence) {
i = fence.end;
continue;
}
if (text[i] !== "`") {
i += 1;
continue;
}
const runStart = i;
let runLength = 0;
while (i < text.length && text[i] === "`") {
runLength += 1;
i += 1;
}
if (!open) {
open = true;
ticks = runLength;
openStart = runStart;
continue;
}
if (runLength === ticks) {
spans.push([openStart, i]);
open = false;
ticks = 0;
openStart = -1;
}
}
if (open) spans.push([openStart, text.length]);
return {
spans,
state: {
open,
ticks
}
};
}
function findFenceSpanAtInclusive(spans, index) {
return spans.find((span) => index >= span.start && index < span.end);
}
function isInsideFenceSpan(index, spans) {
return spans.some((span) => index >= span.start && index < span.end);
}
function isInsideInlineSpan(index, spans) {
return spans.some(([start, end]) => index >= start && index < end);
}
//#endregion
//#region src/shared/text/reasoning-tag-text-partitioner.ts
const REASONING_TAG_RE = /<\s*(\/?)\s*(?:(?:antml:)?(?:think(?:ing)?|thought|reasoning)|antthinking)\b[^<>]*>/gi;
const REASONING_TAG_NAMES = [
"think",
"thinking",
"thought",
"reasoning",
"antthinking",
"antml:think",
"antml:thinking",
"antml:thought",
"antml:reasoning"
];
function createReasoningTagTextPartitioner() {
let buffer = "";
let reasoningDepth = 0;
let strictMode = false;
let emittedVisibleText = false;
let inlineCodeState = createInlineCodeState();
let fenceState;
let hiddenInlineCodeState = createInlineCodeState();
let hiddenFenceState;
let recoverableOpenTagText;
const consume = (final, recoverFullUnclosed) => {
const output = [];
const emit = (kind, text) => {
if (!text) return;
if (kind === "text" && text.trim().length > 0) emittedVisibleText = true;
if (kind === "text") {
const nextCode = buildCodeSpanIndex(text, inlineCodeState, fenceState);
inlineCodeState = nextCode.inlineState;
fenceState = nextCode.fenceState;
} else {
const nextCode = buildCodeSpanIndex(text, hiddenInlineCodeState, hiddenFenceState);
hiddenInlineCodeState = nextCode.inlineState;
hiddenFenceState = nextCode.fenceState;
}
const previous = output[output.length - 1];
if (previous?.kind === kind) {
previous.text += text;
return;
}
output.push({
kind,
text
});
};
while (buffer) {
const activeInlineCodeState = reasoningDepth === 0 ? inlineCodeState : hiddenInlineCodeState;
const activeFenceState = reasoningDepth === 0 ? fenceState : hiddenFenceState;
const codeSpans = buildCodeSpanIndex(buffer, activeInlineCodeState, activeFenceState);
const hasUnclosedCode = reasoningDepth === 0 && Boolean(codeSpans.inlineState.open || codeSpans.fenceState.open);
const hasRawReasoning = hasRawReasoningTag(buffer);
const tag = findNextReasoningTag(buffer, (index) => final && hasUnclosedCode && hasRawReasoning ? false : codeSpans.isInside(index));
if (!tag) {
if (final) {
const recoverAsText = reasoningDepth > 0 && recoverFullUnclosed && !hasRawReasoningCloseTag(buffer);
const recoveredText = recoverAsText && recoverableOpenTagText ? recoverableOpenTagText + buffer : buffer;
emit(reasoningDepth > 0 && !recoverAsText ? "thinking" : "text", recoveredText);
buffer = "";
reasoningDepth = 0;
recoverableOpenTagText = void 0;
return output;
}
if (reasoningDepth > 0 && recoverFullUnclosed && (!emittedVisibleText || recoverableOpenTagText)) return output;
if (hasUnclosedCode && hasRawReasoning) {
const openCodeIndex = inlineCodeState.open || fenceState?.open ? 0 : findOpenCodeContextStart(buffer);
if (openCodeIndex !== -1) {
emit("text", buffer.slice(0, openCodeIndex));
buffer = buffer.slice(openCodeIndex);
return output;
}
}
const trailingFenceStart = findTrailingFenceFragmentStart(buffer, activeInlineCodeState, activeFenceState);
if (trailingFenceStart !== -1) {
emit(reasoningDepth > 0 ? "thinking" : "text", buffer.slice(0, trailingFenceStart));
buffer = buffer.slice(trailingFenceStart);
return output;
}
const keepFrom = reasoningTagPrefixSuffixIndex(buffer, (index) => codeSpans.isInside(index));
if (keepFrom === -1) {
emit(reasoningDepth > 0 ? "thinking" : "text", buffer);
buffer = "";
return output;
}
if (reasoningDepth === 0 && keepFrom > 0 && buffer.slice(0, keepFrom).trim().length > 0 && isReasoningCloseTagPrefix(buffer.slice(keepFrom))) return output;
if (keepFrom > 0) {
emit(reasoningDepth > 0 ? "thinking" : "text", buffer.slice(0, keepFrom));
buffer = buffer.slice(keepFrom);
}
return output;
}
const beforeTag = buffer.slice(0, tag.index);
const afterTag = buffer.slice(tag.index + tag.text.length);
if (tag.isClose && reasoningDepth === 0) {
if (recoverFullUnclosed && beforeTag.trim().length > 0 && afterTag.trim().length > 0) {
emit("text", beforeTag + tag.text);
buffer = afterTag;
continue;
}
if (beforeTag.trim().length > 0 && afterTag.trim().length === 0 && !final) return output;
if (beforeTag.trim().length === 0 || afterTag.trim().length === 0) emit("text", beforeTag);
buffer = afterTag;
continue;
}
emit(reasoningDepth > 0 ? "thinking" : "text", buffer.slice(0, tag.index));
buffer = afterTag;
if (tag.isClose) {
reasoningDepth = Math.max(0, reasoningDepth - 1);
if (reasoningDepth === 0) {
recoverableOpenTagText = void 0;
hiddenInlineCodeState = createInlineCodeState();
hiddenFenceState = void 0;
}
} else {
if (reasoningDepth === 0) {
recoverableOpenTagText = recoverFullUnclosed && emittedVisibleText ? tag.text : void 0;
hiddenInlineCodeState = createInlineCodeState();
hiddenFenceState = void 0;
}
reasoningDepth += 1;
}
}
return output;
};
return {
markStrict() {
strictMode = true;
},
push(chunk) {
strictMode = true;
buffer += chunk;
return consume(false, false);
},
pushVisible(chunk) {
buffer += chunk;
return consume(false, true);
},
flush() {
return consume(true, !strictMode);
},
hasPending() {
return buffer.length > 0 || reasoningDepth > 0;
},
isInsideReasoning() {
return reasoningDepth > 0;
}
};
}
function hasRawReasoningTag(text) {
REASONING_TAG_RE.lastIndex = 0;
return REASONING_TAG_RE.test(text);
}
function hasRawReasoningCloseTag(text) {
REASONING_TAG_RE.lastIndex = 0;
for (;;) {
const match = REASONING_TAG_RE.exec(text);
if (!match) return false;
if (match[1] === "/") return true;
}
}
function findNextReasoningTag(text, isIndexInsideCode) {
REASONING_TAG_RE.lastIndex = 0;
for (;;) {
const match = REASONING_TAG_RE.exec(text);
if (!match) return null;
if (!isIndexInsideCode(match.index)) return {
index: match.index,
text: match[0],
isClose: match[1] === "/"
};
}
}
function reasoningTagPrefixSuffixIndex(text, isIndexInsideCode) {
for (let index = text.lastIndexOf("<"); index >= 0;) {
if (!isIndexInsideCode(index) && isReasoningTagPrefix(text.slice(index))) return index;
if (index === 0) break;
index = text.lastIndexOf("<", index - 1);
}
return -1;
}
function isReasoningTagPrefix(text) {
const name = normalizeReasoningTagPrefixName(text);
return REASONING_TAG_NAMES.some((tagName) => {
if (tagName.startsWith(name)) return true;
if (!name.startsWith(tagName)) return false;
const rest = name.slice(tagName.length);
return rest.length === 0 || /^[\s/>]/.test(rest);
});
}
function isReasoningCloseTagPrefix(text) {
return text.replace(/^<\s*/, "<").replace(/^<\s*\//, "</").replace(/^<\/\s*/, "</").toLowerCase().startsWith("</") && isReasoningTagPrefix(text);
}
function normalizeReasoningTagPrefixName(text) {
const normalized = text.replace(/^<\s*/, "<").replace(/^<\s*\//, "</").replace(/^<\/\s*/, "</").toLowerCase();
return (normalized.startsWith("</") ? normalized.slice(2) : normalized.slice(1)).trimStart();
}
function findOpenCodeContextStart(text) {
const fence = findOpenFenceStart(text);
const inline = findOpenInlineCodeStart(text);
if (fence === -1) return inline;
if (inline === -1) return fence;
return Math.min(fence, inline);
}
function findOpenInlineCodeStart(text) {
let openStart = -1;
let openTicks = 0;
let index = 0;
while (index < text.length) {
if (text[index] !== "`") {
index += 1;
continue;
}
const runStart = index;
let runLength = 0;
while (index < text.length && text[index] === "`") {
runLength += 1;
index += 1;
}
if (openStart === -1) {
openStart = runStart;
openTicks = runLength;
} else if (runLength === openTicks) {
openStart = -1;
openTicks = 0;
}
}
return openStart;
}
function findOpenFenceStart(text) {
const fenceRe = /(^|\n)(```|~~~)[^\n]*(?:\n|$)/g;
let open = null;
for (const match of text.matchAll(fenceRe)) {
const index = (match.index ?? 0) + match[1].length;
const marker = match[2] ?? "";
if (open !== null && open.marker === marker) open = null;
else if (!open) open = {
marker,
index
};
}
return open?.index ?? -1;
}
function findTrailingFenceFragmentStart(text, inlineState, fenceState) {
if (inlineState.open || fenceState?.open) return -1;
const lineStart = Math.max(text.lastIndexOf("\n") + 1, 0);
return text.slice(lineStart).match(/^( {0,3})(`{1,2}|~{1,2})$/) ? lineStart : -1;
}
//#endregion
//#region src/llm/providers/openai-completions.ts
/**
* Check if conversation messages contain tool calls or tool results.
* This is needed because Anthropic (via proxy) requires the tools param
* to be present when messages include tool_calls or tool role messages.
*/
function hasToolHistory(messages) {
for (const msg of messages) {
if (msg.role === "toolResult") return true;
if (msg.role === "assistant") {
if (msg.content.some((block) => block.type === "toolCall")) return true;
}
}
return false;
}
function isTextContentBlock(block) {
return block.type === "text";
}
function isThinkingContentBlock(block) {
return block.type === "thinking";
}
function isToolCallBlock(block) {
return block.type === "toolCall";
}
function isImageContentBlock(block) {
return block.type === "image";
}
function resolveCacheRetention(cacheRetention) {
if (cacheRetention) return cacheRetention;
if (typeof process !== "undefined" && process.env.OPENCLAW_CACHE_RETENTION === "long") return "long";
return "short";
}
const streamOpenAICompletions = (model, context, options) => {
const stream = new AssistantMessageEventStream();
(async () => {
const output = {
role: "assistant",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
},
stopReason: "stop",
timestamp: Date.now()
};
try {
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
const compat = getCompat(model);
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
const cacheSessionId = cacheRetention === "none" ? void 0 : options?.sessionId;
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
let params = buildParams(model, context, options, compat, cacheRetention);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== void 0) params = nextParams;
const requestOptions = {
...options?.signal ? { signal: options.signal } : {},
...options?.timeoutMs !== void 0 ? { timeout: options.timeoutMs } : {},
...options?.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {}
};
const { data: openaiStream, response } = await client.chat.completions.create(params, requestOptions).withResponse();
await options?.onResponse?.({
status: response.status,
headers: headersToRecord(response.headers)
}, model);
stream.push({
type: "start",
partial: output
});
let textBlock = null;
let thinkingBlock = null;
let hasFinishReason = false;
const toolCallBlocksByIndex = /* @__PURE__ */ new Map();
const toolCallBlocksById = /* @__PURE__ */ new Map();
const blocks = output.content;
const getContentIndex = (block) => blocks.indexOf(block);
const finishBlock = (block) => {
const contentIndex = getContentIndex(block);
if (contentIndex === -1) return;
if (block.type === "text") stream.push({
type: "text_end",
contentIndex,
content: block.text,
partial: output
});
else if (block.type === "thinking") stream.push({
type: "thinking_end",
contentIndex,
content: block.thinking,
partial: output
});
else if (block.type === "toolCall") {
block.arguments = parseStreamingJson(block.partialArgs);
delete block.partialArgs;
delete block.streamIndex;
stream.push({
type: "toolcall_end",
contentIndex,
toolCall: block,
partial: output
});
}
};
const ensureTextBlock = () => {
if (!textBlock) {
textBlock = {
type: "text",
text: ""
};
blocks.push(textBlock);
stream.push({
type: "text_start",
contentIndex: getContentIndex(textBlock),
partial: output
});
}
return textBlock;
};
const ensureThinkingBlock = (thinkingSignature) => {
if (!thinkingBlock) {
thinkingBlock = {
type: "thinking",
thinking: "",
thinkingSignature
};
blocks.push(thinkingBlock);
stream.push({
type: "thinking_start",
contentIndex: getContentIndex(thinkingBlock),
partial: output
});
}
return thinkingBlock;
};
const appendTextDelta = (delta) => {
const block = ensureTextBlock();
block.text += delta;
stream.push({
type: "text_delta",
contentIndex: getContentIndex(block),
delta,
partial: output
});
};
const appendThinkingDelta = (thinkingSignature, delta) => {
const block = ensureThinkingBlock(thinkingSignature);
block.thinking += delta;
stream.push({
type: "thinking_delta",
contentIndex: getContentIndex(block),
delta,
partial: output
});
};
const ensureToolCallBlock = (toolCall) => {
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : void 0;
let block = streamIndex !== void 0 ? toolCallBlocksByIndex.get(streamIndex) : void 0;
if (!block && toolCall.id) block = toolCallBlocksById.get(toolCall.id);
if (!block) {
block = {
type: "toolCall",
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: {},
partialArgs: "",
streamIndex
};
if (streamIndex !== void 0) toolCallBlocksByIndex.set(streamIndex, block);
if (toolCall.id) toolCallBlocksById.set(toolCall.id, block);
blocks.push(block);
stream.push({
type: "toolcall_start",
contentIndex: getContentIndex(block),
partial: output
});
}
if (streamIndex !== void 0 && block.streamIndex === void 0) {
block.streamIndex = streamIndex;
toolCallBlocksByIndex.set(streamIndex, block);
}
if (toolCall.id) toolCallBlocksById.set(toolCall.id, block);
return block;
};
const reasoningTagTextPartitioner = createReasoningTagTextPartitioner();
const appendPartitionedContent = (text, hasMirroredReasoning) => {
const routedDeltas = hasMirroredReasoning ? reasoningTagTextPartitioner.push(text) : reasoningTagTextPartitioner.pushVisible(text);
for (const delta of routedDeltas) if (delta.kind === "text") appendTextDelta(delta.text);
};
const flushPartitionedContent = () => {
for (const delta of reasoningTagTextPartitioner.flush()) if (delta.kind === "text") appendTextDelta(delta.text);
};
for await (const chunk of openaiStream) {
if (!chunk || typeof chunk !== "object") continue;
output.responseId ||= chunk.id;
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) output.responseModel ||= chunk.model;
if (chunk.usage) output.usage = parseChunkUsage(chunk.usage, model);
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : void 0;
if (!choice) continue;
const choiceUsage = choice.usage;
if (!chunk.usage && choiceUsage) output.usage = parseChunkUsage(choiceUsage, model);
if (choice.finish_reason) {
const finishReasonResult = mapStopReason(choice.finish_reason);
output.stopReason = finishReasonResult.stopReason;
if (finishReasonResult.errorMessage) output.errorMessage = finishReasonResult.errorMessage;
hasFinishReason = true;
}
if (choice.delta) {
const reasoningFields = [
"reasoning_content",
"reasoning",
"reasoning_text"
];
const deltaFields = choice.delta;
const shouldEmitReasoning = Boolean(model.reasoning && options?.reasoningEffort);
let foundReasoningField = null;
for (const field of reasoningFields) {
const value = deltaFields[field];
if (typeof value === "string" && value.length > 0) {
foundReasoningField = field;
break;
}
}
if (foundReasoningField) reasoningTagTextPartitioner.markStrict();
if (choice.delta.content !== null && choice.delta.content !== void 0 && choice.delta.content.length > 0) appendPartitionedContent(choice.delta.content, Boolean(foundReasoningField));
if (shouldEmitReasoning && foundReasoningField) {
const delta = deltaFields[foundReasoningField];
if (typeof delta === "string" && delta.length > 0) appendThinkingDelta(model.provider === "opencode-go" && foundReasoningField === "reasoning" ? "reasoning_content" : foundReasoningField, delta);
}
if (choice?.delta?.tool_calls) {
flushPartitionedContent();
for (const toolCall of choice.delta.tool_calls) {
const block = ensureToolCallBlock(toolCall);
if (!block.id && toolCall.id) {
block.id = toolCall.id;
toolCallBlocksById.set(toolCall.id, block);
}
if (!block.name && toolCall.function?.name) block.name = toolCall.function.name;
let delta = "";
if (toolCall.function?.arguments) {
delta = toolCall.function.arguments;
block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
block.arguments = parseStreamingJson(block.partialArgs);
}
stream.push({
type: "toolcall_delta",
contentIndex: getContentIndex(block),
delta,
partial: output
});
}
}
const reasoningDetails = choice.delta.reasoning_details;
if (reasoningDetails && Array.isArray(reasoningDetails)) {
for (const detail of reasoningDetails) if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
const matchingToolCall = output.content.find((b) => b.type === "toolCall" && b.id === detail.id);
if (matchingToolCall) matchingToolCall.thoughtSignature = JSON.stringify(detail);
}
}
}
}
flushPartitionedContent();
for (const block of blocks) finishBlock(block);
if (options?.signal?.aborted) throw new Error("Request was aborted");
if (output.stopReason === "aborted") throw new Error("Request was aborted");
if (output.stopReason === "error") throw new Error(output.errorMessage || "Provider returned an error stop reason");
if (!hasFinishReason) throw new Error("Stream ended without finish_reason");
const hasToolCalls = output.content.some((block) => block.type === "toolCall");
const hasVisibleText = output.content.some((block) => block.type === "text" && block.text.trim().length > 0);
if (output.stopReason === "toolUse" && !hasToolCalls) output.stopReason = "stop";
if (output.stopReason === "stop" && hasToolCalls && !hasVisibleText) output.stopReason = "toolUse";
if (hasToolCalls && output.stopReason !== "toolUse") output.content = output.content.filter((block) => block.type !== "toolCall");
stream.push({
type: "done",
reason: output.stopReason,
message: output
});
stream.end();
} catch (error) {
for (const block of output.content) {
delete block.index;
delete block.partialArgs;
delete block.streamIndex;
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
const rawMetadata = error?.error?.metadata?.raw;
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
stream.push({
type: "error",
reason: output.stopReason,
error: output
});
stream.end();
}
})();
return stream;
};
const streamSimpleOpenAICompletions = (model, context, options) => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
const base = buildBaseOptions(model, options, apiKey);
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : void 0;
const reasoningEffort = clampedReasoning === "off" ? void 0 : clampedReasoning === "max" ? "xhigh" : clampedReasoning;
const toolChoice = options?.toolChoice;
return streamOpenAICompletions(model, context, {
...base,
reasoningEffort,
toolChoice
});
};
function createClient(model, context, apiKey, optionsHeaders, sessionId, compat = getCompat(model)) {
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
const headers = { ...model.headers };
if (model.provider === "github-copilot") {
const hasImages = hasCopilotVisionInput(context.messages);
const copilotHeaders = buildCopilotDynamicHeaders({
messages: context.messages,
hasImages
});
Object.assign(headers, copilotHeaders);
}
if (sessionId && compat.sendSessionAffinityHeaders) {
headers.session_id = sessionId;
headers["x-client-request-id"] = sessionId;
headers["x-session-affinity"] = sessionId;
}
if (optionsHeaders) Object.assign(headers, optionsHeaders);
const defaultHeaders = model.provider === "cloudflare-ai-gateway" ? {
...headers,
Authorization: headers.Authorization ?? null,
"cf-aig-authorization": `Bearer ${apiKey}`
} : headers;
return new OpenAI({
apiKey,
baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl,
dangerouslyAllowBrowser: true,
defaultHeaders
});
}
function buildParams(model, context, options, compat = getCompat(model), cacheRetention = resolveCacheRetention(options?.cacheRetention)) {
const cacheControl = getCompatCacheControl(compat, cacheRetention);
const messages = convertMessages(model, context, compat, { preserveSystemPromptCacheBoundary: cacheControl !== void 0 });
const supportsPromptCacheKey = model.baseUrl.includes("api.openai.com") || compat.supportsPromptCacheKey;
const promptCacheKey = supportsPromptCacheKey && cacheRetention !== "none" ? clampOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId) : void 0;
const params = {
model: model.id,
messages,
stream: true,
prompt_cache_key: promptCacheKey,
prompt_cache_retention: supportsPromptCacheKey && cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : void 0
};
if (compat.supportsUsageInStreaming) params.stream_options = { include_usage: true };
if (compat.supportsStore) params.store = false;
if (options?.maxTokens) {
const maxTokens = clampOpenAICompletionsMaxTokens(model, options.maxTokens);
if (compat.maxTokensField === "max_tokens") params.max_tokens = maxTokens;
else params.max_completion_tokens = maxTokens;
}
if (options?.temperature !== void 0) params.temperature = options.temperature;
if (options?.stop !== void 0 && options.stop.length > 0) params.stop = options.stop;
if (context.tools && context.tools.length > 0) {
params.tools = convertTools(context.tools, compat);
if (compat.zaiToolStream) params.tool_stream = true;
} else if (hasToolHistory(context.messages)) params.tools = [];
if (cacheControl) applyAnthropicCacheControl(messages, params.tools, cacheControl);
if (options?.toolChoice) params.tool_choice = options.toolChoice;
if (compat.thinkingFormat === "zai" && model.reasoning) params.enable_thinking = Boolean(options?.reasoningEffort);
else if (compat.thinkingFormat === "qwen" && model.reasoning) params.enable_thinking = Boolean(options?.reasoningEffort);
else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) params.chat_template_kwargs = {
enable_thinking: Boolean(options?.reasoningEffort),
preserve_thinking: true
};
else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
if (options?.reasoningEffort) params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
} else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
const openRouterParams = params;
if (options?.reasoningEffort) openRouterParams.reasoning = { effort: model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort };
else if (model.thinkingLevelMap?.off !== null) openRouterParams.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
} else if (compat.thinkingFormat === "together" && model.reasoning) {
const togetherParams = params;
togetherParams.reasoning = { enabled: Boolean(options?.reasoningEffort) };
if (options?.reasoningEffort && compat.supportsReasoningEffort) togetherParams.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
else if (!options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
const offValue = model.thinkingLevelMap?.off;
if (typeof offValue === "string") params.reasoning_effort = offValue;
}
if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) params.provider = model.compat.openRouterRouting;
if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
const routing = model.compat.vercelGatewayRouting;
if (routing.only || routing.order) {
const gatewayOptions = {};
if (routing.only) gatewayOptions.only = routing.only;
if (routing.order) gatewayOptions.order = routing.order;
params.providerOptions = { gateway: gatewayOptions };
}
}
return params;
}
function clampOpenAICompletionsMaxTokens(model, requestedMaxTokens) {
const modelMaxTokens = typeof model.maxTokens === "number" && Number.isFinite(model.maxTokens) && model.maxTokens > 0 ? Math.floor(model.maxTokens) : void 0;
return modelMaxTokens === void 0 || requestedMaxTokens <= modelMaxTokens ? requestedMaxTokens : modelMaxTokens;
}
function getCompatCacheControl(compat, cacheRetention) {
if (compat.cacheControlFormat !== "anthropic" || cacheRetention === "none") return;
const ttl = cacheRetention === "long" && compat.supportsLongCacheRetention ? "1h" : void 0;
return {
type: "ephemeral",
...ttl ? { ttl } : {}
};
}
function applyAnthropicCacheControl(messages, tools, cacheControl) {
addCacheControlToSystemPrompt(messages, cacheControl);
addCacheControlToLastTool(tools, cacheControl);
addCacheControlToLastConversationMessage(messages, cacheControl);
}
function addCacheControlToSystemPrompt(messages, cacheControl) {
for (const message of messages) if (message.role === "system" || message.role === "developer") {
addCacheControlToInstructionMessage(message, cacheControl);
return;
}
}
function addCacheControlToLastConversationMessage(messages, cacheControl) {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === "user" || message.role === "assistant") {
if (addCacheControlToMessage(message, cacheControl)) return;
}
}
}
function addCacheControlToLastTool(tools, cacheControl) {
if (!tools || tools.length === 0) return;
const lastTool = tools[tools.length - 1];
lastTool.cache_control = cacheControl;
}
function addCacheControlToInstructionMessage(message, cacheControl) {
return addCacheControlToTextContent(message, cacheControl);
}
function addCacheControlToMessage(message, cacheControl) {
if (message.role === "user" || message.role === "assistant") return addCacheControlToTextContent(message, cacheControl);
return false;
}
function addCacheControlToTextContent(message, cacheControl) {
const content = message.content;
if (typeof content === "string") {
if (content.length === 0) return false;
message.content = buildCacheControlledTextParts(content, cacheControl);
return true;
}
if (!Array.isArray(content)) return false;
for (let i = content.length - 1; i >= 0; i--) {
const part = content[i];
if (part?.type === "text") {
const text = part.text;
content.splice(i, 1, ...buildCacheControlledTextParts(text, cacheControl));
return true;
}
}
return false;
}
function buildCacheControlledTextParts(text, cacheControl) {
const split = splitSystemPromptCacheBoundary(text);
if (!split) return [{
type: "text",
text,
cache_control: cacheControl
}];
const parts = [];
if (split.stablePrefix) parts.push({
type: "text",
text: split.stablePrefix,
cache_control: cacheControl
});
if (split.dynamicSuffix) parts.push({
type: "text",
text: split.dynamicSuffix
});
return parts.length > 0 ? parts : [{
type: "text",
text: ""
}];
}
function convertMessages(model, context, compat, options = {}) {
const params = [];
const normalizeToolCallId = (id) => {
if (id.includes("|")) {
const [callId] = id.split("|");
return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
}
if (model.provider === "openai") return id.length > 40 ? id.slice(0, 40) : id;
return id;
};
const transformedMessages = transformMessages(context.messages, model, (id) => normalizeToolCallId(id));
if (context.systemPrompt) {
const role = model.reasoning && compat.supportsDeveloperRole ? "developer" : "system";
const systemPrompt = options.preserveSystemPromptCacheBoundary ? context.systemPrompt : stripSystemPromptCacheBoundary(context.systemPrompt);
params.push({
role,
content: sanitizeSurrogates(systemPrompt)
});
}
let lastRole = null;
for (let i = 0; i < transformedMessages.length; i++) {
const msg = transformedMessages[i];
if (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") params.push({
role: "assistant",
content: "I have processed the tool results."
});
if (msg.role === "user") if (typeof msg.content === "string") params.push({
role: "user",
content: sanitizeSurrogates(msg.content)
});
else {
const content = msg.content.map((item) => {
if (item.type === "text") return {
type: "text",
text: sanitizeSurrogates(item.text)
};
return {
type: "image_url",
image_url: { url: `data:${item.mimeType};base64,${item.data}` }
};
});
if (content.length === 0) continue;
params.push({
role: "user",
content
});
}
else if (msg.role === "assistant") {
const assistantMsg = {
role: "assistant",
content: compat.requiresAssistantAfterToolResult ? "" : null
};
const assistantTextParts = msg.content.filter(isTextContentBlock).filter((block) => block.text.trim().length > 0).map((block) => ({
type: "text",
text: sanitizeSurrogates(block.text)
}));
const assistantText = assistantTextParts.map((part) => part.text).join("");
const nonEmptyThinkingBlocks = msg.content.filter(isThinkingContentBlock).filter((block) => block.thinking.trim().length > 0);
if (nonEmptyThinkingBlocks.length > 0) if (compat.requiresThinkingAsText) assistantMsg.content = [{
type: "text",
text: nonEmptyThinkingBlocks.map((block) => sanitizeSurrogates(block.thinking)).join("\n\n")
}, ...assistantTextParts];
else {
if (assistantText.length > 0) assistantMsg.content = assistantText;
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
if (model.provider === "opencode-go" && signature === "reasoning") signature = "reasoning_content";
if (signature && signature.length > 0) assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
}
else if (assistantText.length > 0) assistantMsg.content = assistantText;
const toolCalls = msg.content.filter(isToolCallBlock);
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls.map((tc) => ({
id: tc.id,
type: "function",
function: {
name: tc.name,
arguments: JSON.stringify(tc.arguments)
}
}));
const reasoningDetails = toolCalls.filter((tc) => tc.thoughtSignature).map((tc) => {
try {
return JSON.parse(tc.thoughtSignature);
} catch {
return null;
}
}).filter(Boolean);
if (reasoningDetails.length > 0) assistantMsg.reasoning_details = reasoningDetails;
}
if (compat.requiresReasoningContentOnAssistantMessages && model.reasoning && assistantMsg.reasoning_content === void 0) assistantMsg.reasoning_content = "";
const content = assistantMsg.content;
if (!(content !== null && content !== void 0 && (typeof content === "string" ? content.length > 0 : content.length > 0)) && !assistantMsg.tool_calls) continue;
params.push(assistantMsg);
} else if (msg.role === "toolResult") {
const imageBlocks = [];
let j = i;
for (; j < transformedMessages.length && transformedMessages[j].role === "toolResult"; j++) {
const toolMsg = transformedMessages[j];
const textResult = toolMsg.content.filter(isTextContentBlock).map((block) => block.text).join("\n");
const hasImages = toolMsg.content.some((c) => c.type === "image");
const toolResultMsg = {
role: "tool",
content: sanitizeSurrogates(textResult.length > 0 ? textResult : "(see attached image)"),
tool_call_id: toolMsg.toolCallId
};
if (compat.requiresToolResultName && toolMsg.toolName) toolResultMsg.name = toolMsg.toolName;
params.push(toolResultMsg);
if (hasImages && model.input.includes("image")) {
for (const block of toolMsg.content) if (isImageContentBlock(block)) imageBlocks.push({
type: "image_url",
image_url: { url: `data:${block.mimeType};base64,${block.data}` }
});
}
}
i = j - 1;
if (imageBlocks.length > 0) {
if (compat.requiresAssistantAfterToolResult) params.push({
role: "assistant",
content: "I have processed the tool results."
});
params.push({
role: "user",
content: [{
type: "text",
text: "Attached image(s) from tool result:"
}, ...imageBlocks]
});
lastRole = "user";
} else lastRole = "toolResult";
continue;
}
lastRole = msg.role;
}
return params;
}
function convertTools(tools, compat) {
return tools.map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
...compat.supportsStrictMode && { strict: false }
}
}));
}
function parseChunkUsage(rawUsage, model) {
const promptTokens = rawUsage.prompt_tokens || 0;
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
const outputTokens = rawUsage.completion_tokens || 0;
const usage = {
input,
output: outputTokens,
cacheRead: cacheReadTokens,
cacheWrite: cacheWriteTokens,
totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
};
calculateCost(model, usage);
return usage;
}
function mapStopReason(reason) {
if (reason === null) return { stopReason: "stop" };
switch (reason) {
case "stop":
case "end": return { stopReason: "stop" };
case "length": return { stopReason: "length" };
case "function_call":
case "tool_calls": return { stopReason: "toolUse" };
case "content_filter": return {
stopReason: "error",
errorMessage: "Provider finish_reason: content_filter"
};
case "network_error": return {
stopReason: "error",
errorMessage: "Provider finish_reason: network_error"
};
default: return {
stopReason: "error",
errorMessage: `Provider finish_reason: ${reason}`
};
}
}
/**
* Detect compatibility settings from provider and baseUrl for known providers.
* Provider takes precedence over URL-based detection since it's explicitly configured.
* Returns a fully resolved OpenAICompletionsCompat object with all fields set.
*/
function detectCompat(model) {
const provider = model.provider;
const baseUrl = model.baseUrl;
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz");
const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
const isNonStandard = provider === "cerebras" || baseUrl.includes("cerebras.ai") || provider === "xai" || baseUrl.includes("api.x.ai") || isTogether || baseUrl.includes("chutes.ai") || baseUrl.includes("deepseek.com") || isZai || isMoonshot || provider === "opencode" || baseUrl.includes("opencode.ai") || isCloudflareWorkersAI || isCloudflareAiGateway;
const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether;
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
const isXiaomi = provider === "xiaomi" || baseUrl.includes("xiaomimimo.com");
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : void 0;
return {
supportsStore: !isNonStandard,
supportsDeveloperRole: !isNonStandard,
supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway,
supportsUsageInStreaming: true,
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresThinkingAsText: false,
requiresReasoningContentOnAssistantMessages: isDeepSeek || isXiaomi,
thinkingFormat: isDeepSeek ? "deepseek" : isXiaomi ? "deepseek" : isZai ? "zai" : isTogether ? "together" : provider === "openrouter" || baseUrl.includes("openrouter.ai") ? "openrouter" : "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
zaiToolStream: false,
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway,
cacheControlFormat,
sendSessionAffinityHeaders: false,
supportsPromptCacheKey: false,
supportsLongCacheRetention: !(isTogether || isCloudflareWorkersAI || isCloudflareAiGateway)
};
}
/**
* Get resolved compatibility settings for a model.
* Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
*/
function getCompat(model) {
const detected = detectCompat(model);
if (!model.compat) return detected;
return {
supportsStore: model.compat.supportsStore ?? detected.supportsStore,
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ?? detected.requiresReasoningContentOnAssistantMessages,
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
supportsPromptCacheKey: model.compat.supportsPromptCacheKey ?? detected.supportsPromptCacheKey,
supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention
};
}
//#endregion
export { buildCodeSpanIndex as a, createReasoningTagTextPartitioner as i, streamOpenAICompletions as n, createInlineCodeState as o, streamSimpleOpenAICompletions as r, convertMessages as t };