@openclaw/ai 2026.10.1-beta.2 → 2026.10.5-beta.1
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 +6 -0
- package/dist/{anthropic-CZv6XFER.mjs → anthropic-BAHKgWam.mjs} +30 -59
- package/dist/{anthropic-payload-policy-BhCcgNdm.d.mts → anthropic-payload-policy-DGblkHL5.d.mts} +19 -16
- package/dist/{anthropic-stream-reducer-BQ1tjd7x.mjs → anthropic-stream-reducer-g6w1YVii.mjs} +197 -285
- package/dist/{api-registry-DpU0Rj-I.d.mts → api-registry-BhXXYdRA.d.mts} +2 -2
- package/dist/assistant-message-events-CHMJgbhs.mjs +18 -0
- package/dist/{assistant-output-OLm-Oln0.mjs → assistant-output-CUEeSotf.mjs} +2 -2
- package/dist/{assistant-text-phase-Bl1aDCB7.mjs → assistant-text-phase-BrV8KdFO.mjs} +7 -6
- package/dist/{azure-openai-responses-DH7bd3QT.mjs → azure-openai-responses-C7qBuR23.mjs} +8 -9
- package/dist/{credential-redaction-BKv49aiv.d.mts → credential-redaction-BHxruEH6.d.mts} +3 -1
- package/dist/{diagnostics-Caj-4RBA.mjs → diagnostics-tQasYct1.mjs} +10 -3
- package/dist/diagnostics.d.mts +7 -7
- package/dist/diagnostics.mjs +3 -3
- package/dist/{env-api-keys-CyX4VKGO.mjs → env-api-keys-CrxwdylR.mjs} +24 -35
- package/dist/event-stream-6UNs-AhN.d.mts +1 -0
- package/dist/{event-stream-DrRM_90c.d.mts → event-stream-BcqWLo_u.d.mts} +1 -1
- package/dist/{event-stream-C5AbCAaZ.mjs → event-stream-X2gmsUWv.mjs} +5 -8
- package/dist/event-stream.d.mts +2 -2
- package/dist/event-stream.mjs +1 -1
- package/dist/google-Boz2QcpK.mjs +20 -0
- package/dist/{google-interactions-wrnE6MLw.mjs → google-interactions-tjAb1mc5.mjs} +52 -84
- package/dist/{google-messages-BwPPycw6.mjs → google-messages-C-RZzloH.mjs} +25 -35
- package/dist/google-provider-stream-CMImyxbX.mjs +58 -0
- package/dist/{google-shared-Eql1OMPU.mjs → google-shared-OyAtMTZv.mjs} +6 -15
- package/dist/google-vertex-hCxTJea7.mjs +40 -0
- package/dist/{host-BinBeGcQ.d.mts → host-CTDz7LXD.d.mts} +6 -3
- package/dist/{host-DCW52iHe.mjs → host-JWXZWa-c.mjs} +12 -8
- package/dist/{host-policy-B3jc4Rpn.mjs → host-policy-q4Azym9F.mjs} +1 -1
- package/dist/{index-a8e_oKnu.d.mts → index-BYDvopUB.d.mts} +27 -6
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +7 -6
- package/dist/internal/anthropic.d.mts +6 -6
- package/dist/internal/anthropic.mjs +5 -5
- package/dist/internal/openai-completions-compat.d.mts +1 -1
- package/dist/internal/openai-completions-compat.mjs +1 -1
- package/dist/internal/openai-responses-payload-policy.d.mts +8 -1
- package/dist/internal/openai-responses-payload-policy.mjs +3 -3
- package/dist/internal/openai.d.mts +9 -9
- package/dist/internal/openai.mjs +9 -11
- package/dist/internal/runtime.d.mts +14 -11
- package/dist/internal/runtime.mjs +10 -13
- package/dist/internal/shared.d.mts +5 -5
- package/dist/internal/shared.mjs +6 -7
- package/dist/internal/tool-schema.mjs +2 -2
- package/dist/{llm-request-activity-D8f_oxHj.mjs → llm-request-activity-CfMuh5cC.mjs} +2 -2
- package/dist/{mistral-DSt4vQAO.mjs → mistral-DUveP4hF.mjs} +71 -146
- package/dist/{model-transport-url-C5f59L3Y.d.mts → model-transport-url-BrsjjdjR.d.mts} +5 -1
- package/dist/{number-coercion-V8-BhME4.mjs → number-coercion-C3hztWoY.mjs} +5 -4
- package/dist/{openai-chatgpt-responses-BC2x0Oqx.mjs → openai-chatgpt-responses-OCwckGhY.mjs} +173 -242
- package/dist/{openai-completions-DNyHnXKg.mjs → openai-completions-DOltBUe9.mjs} +7 -6
- package/dist/{openai-completions-compat-BYdYpXhU.d.mts → openai-completions-compat-i-nLZxFy.d.mts} +10 -11
- package/dist/{openai-completions-compat-BdmlhD4u.mjs → openai-completions-compat-pg1q4uNG.mjs} +10 -6
- package/dist/openai-completions-transport-OMrn7gnZ.mjs +3319 -0
- package/dist/{openai-prompt-cache-I8YQTzf8.mjs → openai-prompt-cache-Cr2xKVrS.mjs} +1 -1
- package/dist/{openai-prompt-cache-45-AH_xv.d.mts → openai-prompt-cache-DIPMrMFy.d.mts} +2 -2
- package/dist/{openai-responses-uZ5oWSp7.mjs → openai-responses-BSBJUA19.mjs} +9 -10
- package/dist/{openai-responses-compaction-replay-BClApUIf.d.mts → openai-responses-compaction-replay-B2FE4Nw8.d.mts} +4 -4
- package/dist/{openai-responses-contracts-Br5IIw_h.d.mts → openai-responses-contracts-DKqMJ1Ct.d.mts} +9 -4
- package/dist/{openai-responses-contracts-BKu8O7l4.mjs → openai-responses-contracts-DlKMMTxK.mjs} +26 -6
- package/dist/{openai-responses-compaction-window-m878aVpU.mjs → openai-responses-endpoint-CinGh4VZ.mjs} +14 -4
- package/dist/{openai-responses-prompt-observer-internal-CCg7B6l4.mjs → openai-responses-request-lifecycle-D2IFiI0-.mjs} +39 -4
- package/dist/{openai-responses-shared-C7oVyjED.mjs → openai-responses-shared-DVGhq5xF.mjs} +1036 -327
- package/dist/{openai-responses-terminal-usage-DgImcyJu.d.mts → openai-responses-terminal-usage-BygjiAdi.d.mts} +2 -2
- package/dist/{openai-tool-schema-DYNePiF8.mjs → openai-tool-schema-DDPXsvJP.mjs} +236 -399
- package/dist/{positive-integer-CwLi8LUL.mjs → positive-integer-CqWjqSi9.mjs} +1 -1
- package/dist/{provider-error-DQBE1bUQ.mjs → provider-error-CF9GWCs2.mjs} +5 -3
- package/dist/{provider-options-D5ABCV_l.d.mts → provider-options-CVrXZBtA.d.mts} +3 -2
- package/dist/{provider-transport-turn-state-_M2kLdUE.mjs → provider-transport-turn-state-LuCaTsSO.mjs} +2 -2
- package/dist/{provider-types-F8Mqc9Ma.d.mts → provider-types-CHtRx0dI.d.mts} +3 -3
- package/dist/provider-types.d.mts +6 -6
- package/dist/providers.d.mts +2 -2
- package/dist/providers.mjs +12 -12
- package/dist/{reasoning-tag-text-partitioner-Ju9VpUGl.mjs → reasoning-tag-text-partitioner-0-Rsezog.mjs} +13 -12
- package/dist/{required-api-key-D4kn3252.mjs → required-api-key-CFJvcFy_.mjs} +1 -1
- package/dist/{simple-options-BkKTXxhp.mjs → simple-options-BARFAzk9.mjs} +9 -11
- package/dist/{src-B0NIt2UK.mjs → src-C_wfIUmV.mjs} +35 -10
- package/dist/{stream-DSdzKsDK.mjs → stream-DwrETW0_.mjs} +12 -21
- package/dist/{stream-first-event-timeout-D9ZmL_s9.mjs → stream-first-event-timeout-Dc42hKv6.mjs} +8 -21
- package/dist/{streaming-byte-guard-CC-HMn_u.mjs → streaming-byte-guard-DBXr1XYn.mjs} +16 -1
- package/dist/{string-normalization-PN10hf6d.mjs → string-normalization-I3ux50s9.mjs} +1 -1
- package/dist/{text-signature-MLwfi6fq.mjs → text-signature-B_MB6U_9.mjs} +32 -5
- package/dist/{tool-schema-json-projection-Ct8SeBeJ.mjs → tool-schema-json-projection-B8sG1I9v.mjs} +29 -23
- package/dist/{transport-stream-shared-BcAKh2XH.d.mts → transport-stream-shared-BmpzDtdQ.d.mts} +4 -4
- package/dist/{transport-stream-shared-CxXXz_Dd.mjs → transport-stream-shared-DH08b7Il.mjs} +31 -20
- package/dist/transports.d.mts +33 -22
- package/dist/transports.mjs +73 -1234
- package/dist/{types-DvzmWW8J.d.mts → types-Bd2T86VJ.d.mts} +5 -23
- package/dist/types-Kdfw_04V.d.mts +1 -0
- package/dist/types.d.mts +6 -6
- package/dist/types.mjs +5 -4
- package/dist/{validation-B3lQt_VE.d.mts → validation-DdGB1yy3.d.mts} +1 -1
- package/dist/{validation-DbqlIfDn.mjs → validation-opC2pjk9.mjs} +2 -3
- package/dist/validation.d.mts +1 -1
- package/dist/validation.mjs +1 -1
- package/package.json +4 -4
- package/dist/event-stream-DqDQWqiy.d.mts +0 -1
- package/dist/google-1WLOtb8w.mjs +0 -54
- package/dist/google-vertex-0LrlRoO3.mjs +0 -88
- package/dist/openai-completions-transport-DWM2mQDB.mjs +0 -2081
- package/dist/openai-responses-request-lifecycle-Fsag-IGZ.mjs +0 -36
- package/dist/openai-transport-params-JFsnIyto.mjs +0 -735
- package/dist/openai-web-search-tools-Cfu0uLyN.mjs +0 -14
- package/dist/provider-replay-context-C7Bmi84a.mjs +0 -25
- package/dist/types-BuiEBM3u.d.mts +0 -1
|
@@ -0,0 +1,3319 @@
|
|
|
1
|
+
import { _ as runtimeContextContentToText, f as hasRuntimeContextMarker, p as isRuntimeContextMessage } from "./types-CQc6JNG4.mjs";
|
|
2
|
+
import { r as appendAssistantThinking } from "./event-stream-X2gmsUWv.mjs";
|
|
3
|
+
import { i as asOptionalRecord, o as isRecord, r as asOptionalObjectRecord } from "./record-coerce-CNDovvft.mjs";
|
|
4
|
+
import { s as readNonEmptyStringPreservingWhitespace, t as hasNonEmptyString } from "./string-coerce-CtgOVOSv.mjs";
|
|
5
|
+
import { n as asNonNegativeFiniteNumber, r as asPositiveFiniteNumber } from "./number-coercion-C3hztWoY.mjs";
|
|
6
|
+
import { N as isImageWithMediaPayload, a as resolveAiTransportHeaderSentinels, r as getAiTransportHost } from "./host-JWXZWa-c.mjs";
|
|
7
|
+
import { M as resolveOpenAIThinkingApi, S as isOpenAIGpt6Model, _ as isResponsesServiceTierRejection, b as isOpenAIGpt55Model, d as OpenAIResponsesWebSocketSafeRetryError, g as responsesServiceTierObserver, k as supportsOpenAITemperature, l as OpenAIResponsesWebSocketPostDispatchError, m as parseOpenAIResponsesWebSocketServerError, r as OPENAI_CODEX_RESPONSES_DEFAULT_INSTRUCTIONS, t as AZURE_RESPONSES_FIRST_EVENT_TIMEOUT_MS, u as OpenAIResponsesWebSocketPreDispatchError, v as nextResponsesServiceTier, x as isOpenAIGpt56Model, y as isOpenAIGpt54MiniModel } from "./openai-responses-contracts-DlKMMTxK.mjs";
|
|
8
|
+
import { r as uniqueStrings } from "./string-normalization-I3ux50s9.mjs";
|
|
9
|
+
import { n as getSessionResourceOwnerId, r as registerSessionResourceCleanup } from "./session-resources-D1wgDQ8C.mjs";
|
|
10
|
+
import { c as stableStringify, s as truncateUtf16Safe } from "./provider-error-CF9GWCs2.mjs";
|
|
11
|
+
import { r as resolveOpenAIPromptCacheParams } from "./openai-prompt-cache-Cr2xKVrS.mjs";
|
|
12
|
+
import { d as hasOnlyResponsesFunctionTools, i as emitModelTransportError, r as emitModelTransportDebug, t as formatModelTransportDebugBaseUrl } from "./diagnostics-tQasYct1.mjs";
|
|
13
|
+
import { f as supportsModelTools, i as providerReplayContextMatches, n as buildProviderReplayContext, o as estimateStringChars, r as isProviderReplayContext } from "./text-signature-B_MB6U_9.mjs";
|
|
14
|
+
import { c as resolveOpenAIResponsesTextFormat, i as resolveOpenAICompletionsCompat, l as shouldOmitOllamaCompatResponseFormat, n as isNativeOpenAIEndpoint, o as usesNativeOpenAICodexResponsesBackend, r as isOpenAICodexResponsesModel, s as resolveOpenAICompletionsResponseFormat, t as detectOpenAICompletionsCompat } from "./openai-completions-compat-pg1q4uNG.mjs";
|
|
15
|
+
import { i as resolveProviderEndpoint, t as buildGuardedModelFetch } from "./host-policy-q4Azym9F.mjs";
|
|
16
|
+
import { i as resolveCacheRetention, r as buildCopilotDynamicHeaders } from "./cloudflare-5qSGvIUH.mjs";
|
|
17
|
+
import { E as createToolArgumentPreviewSchedule, M as racePromiseWithAbortSignal, O as parseStreamingJson, P as sanitizeSurrogates, S as withProviderResponseHook, b as transportAbortError, c as failTransportStream, d as iterateModelStream, h as notifyProviderStreamOpened, l as finalizeTerminalToolCallArguments, s as createWritableTransportEventStream, u as finalizeTransportStream, v as sanitizeTransportPayloadText, y as throwIfModelStreamAborted } from "./transport-stream-shared-DH08b7Il.mjs";
|
|
18
|
+
import { C as stripSystemPromptCacheBoundary, D as sortPromptCacheToolsByName, S as splitSystemPromptRelocatableBoundary, d as describeToolResultMediaPlaceholder, l as codeModeToolSurfaceObserver, m as extractToolResultText, s as prepareModelRequestBody, u as reasoningTagTextPolicy, w as stripSystemPromptRelocatableBoundary, x as splitSystemPromptCacheBoundary } from "./simple-options-BARFAzk9.mjs";
|
|
19
|
+
import { $ as replayResponsesReasoningUpdates, A as summarizeResponsesPayload, At as measureUtf8AppendBytes, B as responsesInputFingerprint, C as readResponsesRecoveryEvent, Ct as toOpenAIResponsesToolChoice, D as logResponsesFailedNoDetails, Dt as createResponseModelTracker, E as ResponsesStreamFailure, Ft as resolvePromptCacheKey, G as responsesContinuationRequestFingerprint, H as recordResponsesContinuationState, I as convertResponsesMessages, It as redactIdentifier, Lt as sha256Hex, Mt as readOpenAICompletionsContentDeltas, Nt as readOpenAICompletionsReasoningBatch, O as safeDebugValue, Ot as isOpenAICompletionsThinkingEnabled, P as buildResponsesInputMessage, Pt as resolveOpenAIClientBaseUrl, Q as isConfigurationUpdate, R as readResponsesInputReplayState, S as isInvalidEncryptedContentError, St as reconcileOpenAIResponsesToolChoice, T as resolveNextResponsesEncryptedContentAttempt, Tt as createOpenAIProviderAcceptanceHook, U as resolveResponsesContinuationRequest, V as claimOpenAIResponsesHttpContinuation, W as responsesContinuationPrefixFingerprint, _t as resolveOpenAISimpleReasoningEffort, a as prepareResponsesTools, at as suppressOpenAIResponsesCompaction, bt as projectOpenAITools, c as observeResponsesStream, ct as buildOpenAISdkClientOptions, et as supportsResponsesReasoningUpdate, ft as getCompat, gt as resolveOpenAIRequestReasoning, ht as resolveOpenAIStrictToolFlagWithDiagnostics, j as recordResponsesContextUsage, jt as parseOpenAICompletionsUsage, k as summarizeOpenAITransportError, kt as log, lt as buildOpenAISdkRequestOptions, mt as resolveCodeModeResponsesVisibleToolNames, n as applyResponsesServiceTierPricing, nt as buildOpenAIResponsesReasoningReplayMetadata, ot as assertCodeModeResponsesToolSurface, s as processResponsesStream, st as buildOpenAIClientHeaders, ut as enforceCodeModeResponsesToolSurface, w as resolveAzureOpenAIApiVersion, wt as GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP, x as createResponsesStreamWithRecovery, xt as reconcileOpenAICompletionsToolChoice, yt as prepareOpenAITools, z as recordResponsesInputReplay } from "./openai-responses-shared-DVGhq5xF.mjs";
|
|
20
|
+
import { n as getEnvApiKey } from "./env-api-keys-CrxwdylR.mjs";
|
|
21
|
+
import { n as transformProviderMessages, t as createAssistantOutput } from "./assistant-output-CUEeSotf.mjs";
|
|
22
|
+
import { a as tagUnresolvedTextAsCommentary, i as tagPendingCommentaryText, n as rememberPendingCommentaryTags, r as tagInterruptedTextPhases, t as clearPendingCommentaryText } from "./assistant-text-phase-BrV8KdFO.mjs";
|
|
23
|
+
import { t as notifyLlmRequestActivity } from "./llm-request-activity-CfMuh5cC.mjs";
|
|
24
|
+
import { n as resolveOpencodeSessionHeaders } from "./session-affinity-DA4WHv1h.mjs";
|
|
25
|
+
import "./types.mjs";
|
|
26
|
+
import { i as createOpenAIProviderClient, n as resolveProviderSimpleCompletionHeaders, r as resolveProviderTransportTurnState, t as filterProviderTurnHeadersForExplicitOpencodeSession } from "./provider-transport-turn-state-LuCaTsSO.mjs";
|
|
27
|
+
import { a as withFirstStreamEventTimeout, i as getFirstStreamEventTimeoutMs, r as getFirstStreamEventTimeoutHandler, t as createFirstStreamEventAbortController } from "./stream-first-event-timeout-Dc42hKv6.mjs";
|
|
28
|
+
import { t as boundResponseBody } from "./streaming-byte-guard-DBXr1XYn.mjs";
|
|
29
|
+
import { f as withPreparedToolSchemaNormalization, r as normalizeOpenAIStrictToolParameters } from "./openai-tool-schema-DDPXsvJP.mjs";
|
|
30
|
+
import { isGoogleGemini3ThinkingLevelModel } from "./internal/google-model-family.mjs";
|
|
31
|
+
import { t as mapOpenAIStopReason } from "./openai-stop-reason-8s70vDXj.mjs";
|
|
32
|
+
import { t as createReasoningTagTextPartitioner } from "./reasoning-tag-text-partitioner-0-Rsezog.mjs";
|
|
33
|
+
import { i as resolveAzureDeploymentNameFromMap, t as isOpenAICompatibleAzureResponsesBaseUrl } from "./azure-openai-responses-client-compat-D0dvJMbZ.mjs";
|
|
34
|
+
import { i as isOpenAIResponsesCompactionOutput, l as resolveOpenAIResponsesPayloadPolicy, n as supportsNativeOpenAIResponsesEndpoint, o as sanitizeResponsesImagePayload, r as createBoundedOpenAIResponsesCompactionFetch, s as applyOpenAIResponsesPayloadPolicy, t as isOfficialOpenAIResponsesBaseUrl } from "./openai-responses-endpoint-CinGh4VZ.mjs";
|
|
35
|
+
import { n as withResponsesRequestAcceptance, r as createResponsesPromptEgressObserver, t as responsesRequestLifecycle } from "./openai-responses-request-lifecycle-D2IFiI0-.mjs";
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import OpenAI, { AzureOpenAI } from "openai";
|
|
38
|
+
import { ResponsesWS } from "openai/resources/responses/ws.js";
|
|
39
|
+
//#region packages/ai/src/transports/deepseek-dsml-grammar.ts
|
|
40
|
+
const DEEPSEEK_DSML_MARKERS = [
|
|
41
|
+
"|",
|
|
42
|
+
"|",
|
|
43
|
+
"||"
|
|
44
|
+
].map((bar) => `${bar}DSML${bar}`);
|
|
45
|
+
const DEEPSEEK_DSML_MARKER_PATTERN = `(${DEEPSEEK_DSML_MARKERS.map((marker) => marker.replaceAll("|", "\\|")).join("|")})`;
|
|
46
|
+
function findEarliestDsmlToken(text, tokens, fromIndex = 0) {
|
|
47
|
+
let best = null;
|
|
48
|
+
for (const token of tokens) {
|
|
49
|
+
const index = text.indexOf(token, fromIndex);
|
|
50
|
+
if (index !== -1 && (!best || index < best.index)) best = {
|
|
51
|
+
index,
|
|
52
|
+
token
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
function longestDsmlTokenPrefixSuffixLength(text, tokens, maxTokenLength) {
|
|
58
|
+
const maxLength = Math.min(text.length, maxTokenLength - 1);
|
|
59
|
+
for (let length = maxLength; length > 0; length--) {
|
|
60
|
+
const suffix = text.slice(text.length - length);
|
|
61
|
+
if (tokens.some((token) => token.startsWith(suffix))) return length;
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region packages/ai/src/transports/deepseek-text-filter.ts
|
|
67
|
+
/**
|
|
68
|
+
* DeepSeek DSML streaming text filter.
|
|
69
|
+
* Removes provider-emitted DSML tool markup while buffering split tag prefixes
|
|
70
|
+
* across streamed chunks.
|
|
71
|
+
*/
|
|
72
|
+
const DSML_KINDS = [
|
|
73
|
+
"tool_use_error",
|
|
74
|
+
"tool_calls",
|
|
75
|
+
"tool_call",
|
|
76
|
+
"function_calls"
|
|
77
|
+
];
|
|
78
|
+
const DSML_OPEN_TOKENS = DEEPSEEK_DSML_MARKERS.flatMap((marker) => DSML_KINDS.map((kind) => `<${marker}${kind}>`));
|
|
79
|
+
const MAX_OPEN_TOKEN_LEN = Math.max(...DSML_OPEN_TOKENS.map((token) => token.length));
|
|
80
|
+
/** Create an incremental text filter that strips DeepSeek DSML tool blocks. */
|
|
81
|
+
function createDeepSeekTextFilter() {
|
|
82
|
+
let buffer = "";
|
|
83
|
+
let closeToken;
|
|
84
|
+
const consume = (final) => {
|
|
85
|
+
const output = [];
|
|
86
|
+
const emit = (text) => {
|
|
87
|
+
if (text) output.push(text);
|
|
88
|
+
};
|
|
89
|
+
while (buffer) {
|
|
90
|
+
if (closeToken) {
|
|
91
|
+
const closeIndex = buffer.indexOf(closeToken);
|
|
92
|
+
if (closeIndex !== -1) {
|
|
93
|
+
buffer = buffer.slice(closeIndex + closeToken.length);
|
|
94
|
+
closeToken = void 0;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const keep = final ? 0 : Math.min(buffer.length, closeToken.length - 1);
|
|
98
|
+
buffer = buffer.slice(buffer.length - keep);
|
|
99
|
+
if (final) closeToken = void 0;
|
|
100
|
+
return output;
|
|
101
|
+
}
|
|
102
|
+
const open = findEarliestDsmlToken(buffer, DSML_OPEN_TOKENS);
|
|
103
|
+
if (open) {
|
|
104
|
+
emit(buffer.slice(0, open.index));
|
|
105
|
+
buffer = buffer.slice(open.index + open.token.length);
|
|
106
|
+
closeToken = open.token.replace("<", "</");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (final) {
|
|
110
|
+
emit(buffer);
|
|
111
|
+
buffer = "";
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
const keep = longestDsmlTokenPrefixSuffixLength(buffer, DSML_OPEN_TOKENS, MAX_OPEN_TOKEN_LEN);
|
|
115
|
+
const emitLength = buffer.length - keep;
|
|
116
|
+
if (emitLength <= 0) return output;
|
|
117
|
+
emit(buffer.slice(0, emitLength));
|
|
118
|
+
buffer = buffer.slice(emitLength);
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
121
|
+
return output;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
push(chunk) {
|
|
125
|
+
buffer += chunk;
|
|
126
|
+
return consume(false);
|
|
127
|
+
},
|
|
128
|
+
flush() {
|
|
129
|
+
return consume(true);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region packages/ai/src/transports/model-max-tokens-params.ts
|
|
135
|
+
/**
|
|
136
|
+
* Max-token parameter normalization across provider/native naming variants.
|
|
137
|
+
* Callers canonicalize aliases before dispatch so payloads cannot carry
|
|
138
|
+
* conflicting limits.
|
|
139
|
+
*/
|
|
140
|
+
const MAX_TOKENS_PARAM_KEYS = [
|
|
141
|
+
"maxTokens",
|
|
142
|
+
"max_completion_tokens",
|
|
143
|
+
"max_tokens"
|
|
144
|
+
];
|
|
145
|
+
/** Resolve the first supported max-token parameter present in a params object. */
|
|
146
|
+
function resolveMaxTokensParam(params) {
|
|
147
|
+
if (!params) return;
|
|
148
|
+
for (const key of MAX_TOKENS_PARAM_KEYS) {
|
|
149
|
+
const resolved = asNonNegativeFiniteNumber(params[key]);
|
|
150
|
+
if (resolved !== void 0) return resolved;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Canonicalize merged params to `maxTokens`, preserving source precedence from
|
|
155
|
+
* left to right across the provided source objects.
|
|
156
|
+
*/
|
|
157
|
+
function canonicalizeMaxTokensParam(params) {
|
|
158
|
+
let resolved;
|
|
159
|
+
for (const source of params.sources) {
|
|
160
|
+
const sourceValue = resolveMaxTokensParam(source);
|
|
161
|
+
if (sourceValue !== void 0) resolved = sourceValue;
|
|
162
|
+
}
|
|
163
|
+
if (resolved === void 0) return;
|
|
164
|
+
for (const key of MAX_TOKENS_PARAM_KEYS) delete params.merged[key];
|
|
165
|
+
params.merged.maxTokens = resolved;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region packages/ai/src/transports/openai-compatible-conversation-turn.ts
|
|
169
|
+
function hasNonEmptyContentPart(part) {
|
|
170
|
+
if (!part || typeof part !== "object") return false;
|
|
171
|
+
const record = part;
|
|
172
|
+
if (record.type === "text") return hasNonEmptyString(record.text);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
function hasNonEmptyMessageContent(content) {
|
|
176
|
+
if (hasNonEmptyString(content)) return true;
|
|
177
|
+
if (!Array.isArray(content)) return false;
|
|
178
|
+
return content.some(hasNonEmptyContentPart);
|
|
179
|
+
}
|
|
180
|
+
/** Returns whether an OpenAI-compatible messages payload contains a usable turn. */
|
|
181
|
+
function hasOpenAICompatibleConversationTurn(messages) {
|
|
182
|
+
if (!Array.isArray(messages)) return false;
|
|
183
|
+
return messages.some((message) => {
|
|
184
|
+
if (!message || typeof message !== "object") return false;
|
|
185
|
+
const record = message;
|
|
186
|
+
if (record.role === "user") return hasNonEmptyMessageContent(record.content);
|
|
187
|
+
if (record.role === "assistant") {
|
|
188
|
+
if (hasNonEmptyMessageContent(record.content)) return true;
|
|
189
|
+
const toolCalls = record.tool_calls;
|
|
190
|
+
return Array.isArray(toolCalls) && toolCalls.some((toolCall) => toolCall && typeof toolCall === "object");
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region packages/ai/src/transports/openai-completions-cache-control.ts
|
|
197
|
+
const shapedPayloads = /* @__PURE__ */ new WeakSet();
|
|
198
|
+
function resolveCompletionsCacheControl(compat, retention, openRouterRoute) {
|
|
199
|
+
if (compat.cacheControlFormat !== "anthropic" || retention === "none") return;
|
|
200
|
+
return {
|
|
201
|
+
type: "ephemeral",
|
|
202
|
+
...retention === "long" && compat.supportsLongCacheRetention && (openRouterRoute || compat.configuredSupportsLongCacheRetention === true) ? { ttl: "1h" } : {}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Shared Chat Completions policy; repeated wrapper application preserves existing checkpoints. */
|
|
206
|
+
function applyCompletionsAnthropicCacheControl(payload, cacheControl = { type: "ephemeral" }, cacheOptOutIndexes = /* @__PURE__ */ new Set(), markTools = true, markMessages = true) {
|
|
207
|
+
if (shapedPayloads.has(payload)) return;
|
|
208
|
+
shapedPayloads.add(payload);
|
|
209
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
210
|
+
const tools = Array.isArray(payload.tools) ? payload.tools.filter(isRecord) : [];
|
|
211
|
+
const blocks = messages.filter(isRecord).flatMap((message) => Array.isArray(message.content) ? message.content.filter(isRecord) : []);
|
|
212
|
+
for (const block of [...tools, ...blocks]) delete block.cache_control;
|
|
213
|
+
if (!cacheControl) return;
|
|
214
|
+
const markText = (message, splitBoundary) => {
|
|
215
|
+
if (typeof message.content === "string" && message.content) message.content = [{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: message.content
|
|
218
|
+
}];
|
|
219
|
+
const content = message.content;
|
|
220
|
+
if (!Array.isArray(content)) return false;
|
|
221
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
222
|
+
const block = content[i];
|
|
223
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string" || !block.text) continue;
|
|
224
|
+
const split = splitBoundary ? splitSystemPromptCacheBoundary(block.text) : void 0;
|
|
225
|
+
if (split) {
|
|
226
|
+
content.splice(i, 1, ...split.stablePrefix ? [{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: split.stablePrefix,
|
|
229
|
+
cache_control: cacheControl
|
|
230
|
+
}] : [], ...split.dynamicSuffix ? [{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: split.dynamicSuffix
|
|
233
|
+
}] : []);
|
|
234
|
+
if (!split.stablePrefix) return false;
|
|
235
|
+
} else block.cache_control = cacheControl;
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
};
|
|
240
|
+
const lastTool = tools.at(-1);
|
|
241
|
+
if (markTools && lastTool) lastTool.cache_control = cacheControl;
|
|
242
|
+
if (!markMessages) return;
|
|
243
|
+
const system = messages.find((message) => isRecord(message) && (message.role === "system" || message.role === "developer"));
|
|
244
|
+
if (isRecord(system)) markText(system, true);
|
|
245
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
246
|
+
const message = messages[i];
|
|
247
|
+
if (!cacheOptOutIndexes.has(i) && isRecord(message) && (message.role === "user" || message.role === "tool") && markText(message, false)) return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region packages/ai/src/transports/openai-completions-string-content.ts
|
|
252
|
+
/**
|
|
253
|
+
* OpenAI Chat Completions compatibility helpers. Some providers only accept
|
|
254
|
+
* role/content messages with plain string content instead of text block arrays.
|
|
255
|
+
*/
|
|
256
|
+
/** Flatten string-only text block content arrays into newline-joined strings. */
|
|
257
|
+
function flattenCompletionMessagesToStringContent(messages) {
|
|
258
|
+
return messages.map((message) => {
|
|
259
|
+
const record = asOptionalObjectRecord(message);
|
|
260
|
+
const content = record?.content;
|
|
261
|
+
if (!Array.isArray(content)) return message;
|
|
262
|
+
const textParts = [];
|
|
263
|
+
for (const item of content) {
|
|
264
|
+
const part = asOptionalObjectRecord(item);
|
|
265
|
+
if (part?.type !== "text" || typeof part.text !== "string") return message;
|
|
266
|
+
textParts.push(part.text);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
...record,
|
|
270
|
+
content: textParts.join("\n")
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Strip completion messages to role/content fields for strict providers. */
|
|
275
|
+
function stripCompletionMessagesToRoleContent(messages) {
|
|
276
|
+
return messages.map((message) => {
|
|
277
|
+
const record = asOptionalRecord(message);
|
|
278
|
+
if (!record) return message;
|
|
279
|
+
return {
|
|
280
|
+
...Object.hasOwn(record, "role") ? { role: record.role } : {},
|
|
281
|
+
...Object.hasOwn(record, "content") ? { content: record.content } : {}
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region packages/ai/src/providers/openai-completions-tool-calls.ts
|
|
287
|
+
const MAX_BUFFERED_TOOL_CALL_ARGUMENT_BYTES = 256e3;
|
|
288
|
+
const MAX_BUFFERED_LEGACY_FOLLOWING_DELTA_BYTES = 256e3;
|
|
289
|
+
const MAX_BUFFERED_LEGACY_FOLLOWING_DELTAS = 1024;
|
|
290
|
+
/** Keep encrypted provider reasoning attached to the first matching tool call. */
|
|
291
|
+
function createOpenAIEncryptedToolCallReasoningTracker() {
|
|
292
|
+
const firstBlocks = /* @__PURE__ */ new Map();
|
|
293
|
+
const pendingDetails = /* @__PURE__ */ new Map();
|
|
294
|
+
return {
|
|
295
|
+
rememberToolCall(id, block, previousId = id) {
|
|
296
|
+
const previous = firstBlocks.get(previousId);
|
|
297
|
+
if (previousId !== id && previous?.block === block) {
|
|
298
|
+
firstBlocks.delete(previousId);
|
|
299
|
+
if (previous.signature && block.thoughtSignature === previous.signature) delete block.thoughtSignature;
|
|
300
|
+
}
|
|
301
|
+
if (!id || firstBlocks.has(id)) return;
|
|
302
|
+
const pendingDetail = pendingDetails.get(id);
|
|
303
|
+
firstBlocks.set(id, {
|
|
304
|
+
block,
|
|
305
|
+
signature: pendingDetail
|
|
306
|
+
});
|
|
307
|
+
if (pendingDetail) {
|
|
308
|
+
block.thoughtSignature = pendingDetail;
|
|
309
|
+
pendingDetails.delete(id);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
consumeDetails(details) {
|
|
313
|
+
if (!Array.isArray(details)) return;
|
|
314
|
+
for (const detail of details) {
|
|
315
|
+
if (!isRecord(detail) || detail.type !== "reasoning.encrypted" || typeof detail.id !== "string" || detail.id.length === 0 || typeof detail.data !== "string" || detail.data.length === 0) continue;
|
|
316
|
+
const serializedDetail = JSON.stringify(detail);
|
|
317
|
+
const matchingBlock = firstBlocks.get(detail.id);
|
|
318
|
+
if (matchingBlock) {
|
|
319
|
+
matchingBlock.signature = serializedDetail;
|
|
320
|
+
matchingBlock.block.thoughtSignature = serializedDetail;
|
|
321
|
+
} else pendingDetails.set(detail.id, serializedDetail);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function hasObservableContent(value) {
|
|
327
|
+
if (typeof value === "string") return value.length > 0;
|
|
328
|
+
if (Array.isArray(value)) return value.some(hasObservableContent);
|
|
329
|
+
if (isRecord(value)) return Object.entries(value).some(([field, nestedValue]) => field !== "type" && field !== "id" && field !== "index" && hasObservableContent(nestedValue));
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
function hasOpenAICompletionsDeltaContent(delta) {
|
|
333
|
+
return Object.entries(delta).some(([field, value]) => field !== "role" && hasObservableContent(value));
|
|
334
|
+
}
|
|
335
|
+
/** Normalize the SDK's legacy single-function lane into its modern tool-call shape. */
|
|
336
|
+
function createOpenAICompletionsToolCallDeltaNormalizer() {
|
|
337
|
+
let sawModernToolCall = false;
|
|
338
|
+
let pendingLegacyArgumentBytes = 0;
|
|
339
|
+
let pendingFollowingDeltaBytes = 0;
|
|
340
|
+
let pendingLegacyToolCall;
|
|
341
|
+
const modernArguments = /* @__PURE__ */ new Map();
|
|
342
|
+
const pendingFollowingDeltas = [];
|
|
343
|
+
const takePendingFollowingDeltas = () => {
|
|
344
|
+
pendingFollowingDeltaBytes = 0;
|
|
345
|
+
return pendingFollowingDeltas.splice(0).map((delta) => ({
|
|
346
|
+
delta,
|
|
347
|
+
toolCalls: []
|
|
348
|
+
}));
|
|
349
|
+
};
|
|
350
|
+
const bufferFollowingDelta = (delta) => {
|
|
351
|
+
const nextDeltaBytes = Buffer.byteLength(JSON.stringify(delta), "utf8");
|
|
352
|
+
if (pendingFollowingDeltaBytes + nextDeltaBytes > MAX_BUFFERED_LEGACY_FOLLOWING_DELTA_BYTES || pendingFollowingDeltas.length >= MAX_BUFFERED_LEGACY_FOLLOWING_DELTAS) throw new Error("Exceeded legacy tool-call content buffer limit");
|
|
353
|
+
pendingFollowingDeltaBytes += nextDeltaBytes;
|
|
354
|
+
pendingFollowingDeltas.push(delta);
|
|
355
|
+
};
|
|
356
|
+
return (delta, finishReason) => {
|
|
357
|
+
const ordinaryDelta = { ...delta };
|
|
358
|
+
delete ordinaryDelta.function_call;
|
|
359
|
+
delete ordinaryDelta.tool_calls;
|
|
360
|
+
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
|
361
|
+
for (const toolCall of delta.tool_calls) {
|
|
362
|
+
const index = typeof toolCall.index === "number" ? toolCall.index : void 0;
|
|
363
|
+
const state = (index === void 0 ? void 0 : modernArguments.get(index)) ?? (toolCall.id ? modernArguments.get(toolCall.id) : void 0) ?? {
|
|
364
|
+
bytes: 0,
|
|
365
|
+
pendingHighSurrogate: false
|
|
366
|
+
};
|
|
367
|
+
if (index !== void 0) modernArguments.set(index, state);
|
|
368
|
+
if (toolCall.id) modernArguments.set(toolCall.id, state);
|
|
369
|
+
const argumentDelta = toolCall.function?.arguments ?? "";
|
|
370
|
+
const append = measureUtf8AppendBytes(state.pendingHighSurrogate, argumentDelta);
|
|
371
|
+
state.bytes += append.bytes;
|
|
372
|
+
if (state.bytes > MAX_BUFFERED_TOOL_CALL_ARGUMENT_BYTES) throw new Error("Exceeded tool-call argument buffer limit");
|
|
373
|
+
state.pendingHighSurrogate = append.endsWithHighSurrogate;
|
|
374
|
+
}
|
|
375
|
+
const precedingDeltas = takePendingFollowingDeltas();
|
|
376
|
+
sawModernToolCall = true;
|
|
377
|
+
pendingLegacyArgumentBytes = 0;
|
|
378
|
+
pendingLegacyToolCall = void 0;
|
|
379
|
+
return [...precedingDeltas, {
|
|
380
|
+
delta: ordinaryDelta,
|
|
381
|
+
toolCalls: delta.tool_calls
|
|
382
|
+
}];
|
|
383
|
+
}
|
|
384
|
+
const functionCall = delta.function_call;
|
|
385
|
+
if (sawModernToolCall || !functionCall && !pendingLegacyToolCall) return [{
|
|
386
|
+
delta: ordinaryDelta,
|
|
387
|
+
toolCalls: []
|
|
388
|
+
}];
|
|
389
|
+
const hadPendingLegacyCall = pendingLegacyToolCall !== void 0;
|
|
390
|
+
const leadingDeltas = [];
|
|
391
|
+
if (hasOpenAICompletionsDeltaContent(ordinaryDelta)) {
|
|
392
|
+
if (hadPendingLegacyCall) bufferFollowingDelta(ordinaryDelta);
|
|
393
|
+
else leadingDeltas.push({
|
|
394
|
+
delta: ordinaryDelta,
|
|
395
|
+
toolCalls: []
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (functionCall && (functionCall.name || functionCall.arguments)) {
|
|
399
|
+
const nextFunctionName = pendingLegacyToolCall?.function?.name ? void 0 : functionCall.name;
|
|
400
|
+
const nextArgumentBytes = Buffer.byteLength(functionCall.arguments ?? "", "utf8") + Buffer.byteLength(nextFunctionName ?? "", "utf8");
|
|
401
|
+
if (pendingLegacyArgumentBytes + nextArgumentBytes > MAX_BUFFERED_TOOL_CALL_ARGUMENT_BYTES) throw new Error("Exceeded tool-call argument buffer limit");
|
|
402
|
+
pendingLegacyArgumentBytes += nextArgumentBytes;
|
|
403
|
+
pendingLegacyToolCall ??= {
|
|
404
|
+
index: 0,
|
|
405
|
+
id: `call_${randomUUID().replaceAll("-", "").slice(0, 24)}`,
|
|
406
|
+
type: "function",
|
|
407
|
+
function: {}
|
|
408
|
+
};
|
|
409
|
+
const pendingFunction = pendingLegacyToolCall.function ??= {};
|
|
410
|
+
if (nextFunctionName) pendingFunction.name = nextFunctionName;
|
|
411
|
+
if (functionCall.arguments) pendingFunction.arguments = (pendingFunction.arguments ?? "") + functionCall.arguments;
|
|
412
|
+
}
|
|
413
|
+
if (!pendingLegacyToolCall) return leadingDeltas.length > 0 ? leadingDeltas : [{
|
|
414
|
+
delta: ordinaryDelta,
|
|
415
|
+
toolCalls: []
|
|
416
|
+
}];
|
|
417
|
+
if (finishReason === "function_call") {
|
|
418
|
+
const confirmedLegacyToolCall = pendingLegacyToolCall;
|
|
419
|
+
pendingLegacyToolCall = void 0;
|
|
420
|
+
pendingLegacyArgumentBytes = 0;
|
|
421
|
+
return [
|
|
422
|
+
...leadingDeltas,
|
|
423
|
+
{
|
|
424
|
+
delta: {},
|
|
425
|
+
toolCalls: [confirmedLegacyToolCall]
|
|
426
|
+
},
|
|
427
|
+
...takePendingFollowingDeltas()
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
if (finishReason) {
|
|
431
|
+
pendingLegacyArgumentBytes = 0;
|
|
432
|
+
pendingLegacyToolCall = void 0;
|
|
433
|
+
return [...leadingDeltas, ...takePendingFollowingDeltas()];
|
|
434
|
+
}
|
|
435
|
+
return leadingDeltas;
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Publish only executable calls; streaming scratch state never belongs in replay. */
|
|
439
|
+
function finalizeOpenAICompletionsToolCalls(output, options = {}) {
|
|
440
|
+
const isToolCall = (block) => block.type === "toolCall";
|
|
441
|
+
const hasToolCalls = output.content.some(isToolCall);
|
|
442
|
+
if (output.stopReason === "toolUse" && !hasToolCalls) output.stopReason = "stop";
|
|
443
|
+
if (output.stopReason === "stop" && hasToolCalls && options.allowSilentToolCallPromotion !== false && !output.content.some((block) => block.type === "text" && block.text.trim().length > 0)) output.stopReason = "toolUse";
|
|
444
|
+
if (output.stopReason !== "toolUse") {
|
|
445
|
+
if (hasToolCalls) output.content = output.content.filter((block) => !isToolCall(block));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const toolCalls = output.content.filter(isToolCall);
|
|
449
|
+
const rejectToolCalls = () => {
|
|
450
|
+
output.stopReason = "error";
|
|
451
|
+
output.errorMessage = "Provider returned an incomplete or malformed tool call";
|
|
452
|
+
output.content = output.content.filter((candidate) => !isToolCall(candidate));
|
|
453
|
+
};
|
|
454
|
+
if (toolCalls.some((call) => typeof call.name !== "string" || call.name.trim().length === 0 || typeof call.partialArgs !== "string" || call.partialArgs.trim().length === 0)) {
|
|
455
|
+
rejectToolCalls();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
finalizeTerminalToolCallArguments(toolCalls, (call) => call.partialArgs);
|
|
460
|
+
} catch {
|
|
461
|
+
rejectToolCalls();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (let contentIndex = 0; contentIndex < output.content.length; contentIndex += 1) {
|
|
465
|
+
const block = output.content[contentIndex];
|
|
466
|
+
if (!block || !isToolCall(block)) continue;
|
|
467
|
+
delete block.partialArgs;
|
|
468
|
+
delete block.streamIndex;
|
|
469
|
+
options.onConfirmedToolCall?.(block, contentIndex);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region packages/ai/src/transports/openai-completions-host.ts
|
|
474
|
+
/**
|
|
475
|
+
* Chat Completions accepts Azure AI Foundry hosts in addition to traditional
|
|
476
|
+
* Azure OpenAI hosts. Do not replace this with isTraditionalAzureOpenAIHost,
|
|
477
|
+
* which intentionally excludes the .services.ai.azure.com Foundry suffix.
|
|
478
|
+
*/
|
|
479
|
+
function isAzureOpenAICompatibleHost(hostname) {
|
|
480
|
+
return hostname.endsWith(".openai.azure.com") || hostname.endsWith(".services.ai.azure.com") || hostname.endsWith(".cognitiveservices.azure.com");
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region packages/ai/src/openai-completions-messages.ts
|
|
484
|
+
/** Whether replayed messages require a tools marker for proxy compatibility. */
|
|
485
|
+
function hasToolCallHistory(messages) {
|
|
486
|
+
return messages.some((message) => message.role === "toolResult" || message.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "toolCall"));
|
|
487
|
+
}
|
|
488
|
+
/** Convert a normalized transcript to OpenAI Chat Completions messages. */
|
|
489
|
+
function convertMessages(model, context, compat, options = {}) {
|
|
490
|
+
const params = [];
|
|
491
|
+
const normalizeToolCallId = (id) => {
|
|
492
|
+
if (id.includes("|")) return id.slice(0, id.indexOf("|")).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
493
|
+
if (model.provider === "openai") return id.length > 40 ? truncateUtf16Safe(id, 40) : id;
|
|
494
|
+
return id;
|
|
495
|
+
};
|
|
496
|
+
const transformedMessages = transformProviderMessages(context.messages, model, normalizeToolCallId);
|
|
497
|
+
let relocatableSplit;
|
|
498
|
+
let systemParamIndex;
|
|
499
|
+
if (context.systemPrompt) {
|
|
500
|
+
const role = model.reasoning && compat.supportsDeveloperRole ? "developer" : "system";
|
|
501
|
+
let systemPrompt;
|
|
502
|
+
if (options.preserveSystemPromptCacheBoundary) systemPrompt = stripSystemPromptRelocatableBoundary(context.systemPrompt);
|
|
503
|
+
else {
|
|
504
|
+
const split = splitSystemPromptRelocatableBoundary(context.systemPrompt);
|
|
505
|
+
if (split && split.relocatable.length > 0) relocatableSplit = split;
|
|
506
|
+
systemPrompt = stripSystemPromptCacheBoundary(context.systemPrompt);
|
|
507
|
+
}
|
|
508
|
+
systemParamIndex = params.length;
|
|
509
|
+
params.push({
|
|
510
|
+
role,
|
|
511
|
+
content: sanitizeSurrogates(systemPrompt)
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
let lastRole = null;
|
|
515
|
+
for (let i = 0; i < transformedMessages.length; i++) {
|
|
516
|
+
const msg = transformedMessages[i];
|
|
517
|
+
if (!msg) continue;
|
|
518
|
+
if (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") params.push({
|
|
519
|
+
role: "assistant",
|
|
520
|
+
content: "I have processed the tool results."
|
|
521
|
+
});
|
|
522
|
+
if (isRuntimeContextMessage(msg)) {
|
|
523
|
+
params.push({
|
|
524
|
+
role: compat.supportsDeveloperRole ? "developer" : "user",
|
|
525
|
+
content: sanitizeSurrogates(runtimeContextContentToText(msg.content))
|
|
526
|
+
});
|
|
527
|
+
options.cacheOptOutIndexes?.add(params.length - 1);
|
|
528
|
+
} else if (msg.role === "user") {
|
|
529
|
+
let userParam;
|
|
530
|
+
if (typeof msg.content === "string") userParam = {
|
|
531
|
+
role: "user",
|
|
532
|
+
content: sanitizeSurrogates(msg.content)
|
|
533
|
+
};
|
|
534
|
+
else {
|
|
535
|
+
const content = msg.content.map((item) => {
|
|
536
|
+
if (item.type === "text") return {
|
|
537
|
+
type: "text",
|
|
538
|
+
text: sanitizeSurrogates(item.text)
|
|
539
|
+
};
|
|
540
|
+
if (item.type === "video") return {
|
|
541
|
+
type: "video_url",
|
|
542
|
+
video_url: { url: `data:${item.mimeType};base64,${item.data}` }
|
|
543
|
+
};
|
|
544
|
+
return {
|
|
545
|
+
type: "image_url",
|
|
546
|
+
image_url: { url: `data:${item.mimeType};base64,${item.data}` }
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
if (content.length === 0) continue;
|
|
550
|
+
userParam = {
|
|
551
|
+
role: "user",
|
|
552
|
+
content
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (hasRuntimeContextMarker(msg)) options.cacheOptOutIndexes?.add(params.length);
|
|
556
|
+
params.push(userParam);
|
|
557
|
+
} else if (msg.role === "assistant") {
|
|
558
|
+
const assistantMsg = {
|
|
559
|
+
role: "assistant",
|
|
560
|
+
content: compat.requiresAssistantAfterToolResult ? "" : null
|
|
561
|
+
};
|
|
562
|
+
const assistantTexts = [];
|
|
563
|
+
const nonEmptyThinkingBlocks = [];
|
|
564
|
+
const toolCalls = [];
|
|
565
|
+
msg.content.forEach((block) => {
|
|
566
|
+
if (block.type === "text" && block.text.trim().length > 0) assistantTexts.push(sanitizeSurrogates(block.text));
|
|
567
|
+
else if (block.type === "thinking" && block.thinking.trim().length > 0) nonEmptyThinkingBlocks.push(block);
|
|
568
|
+
else if (block.type === "toolCall") toolCalls.push(block);
|
|
569
|
+
});
|
|
570
|
+
if (nonEmptyThinkingBlocks.length > 0 && compat.requiresThinkingAsText) assistantMsg.content = [{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: nonEmptyThinkingBlocks.map((block) => sanitizeSurrogates(block.thinking)).join("\n\n")
|
|
573
|
+
}, ...assistantTexts.map((text) => ({
|
|
574
|
+
type: "text",
|
|
575
|
+
text
|
|
576
|
+
}))];
|
|
577
|
+
else {
|
|
578
|
+
const assistantText = assistantTexts.join("\n");
|
|
579
|
+
if (assistantText.length > 0) assistantMsg.content = assistantText;
|
|
580
|
+
if (nonEmptyThinkingBlocks.length > 0) {
|
|
581
|
+
let signature = nonEmptyThinkingBlocks.at(0)?.thinkingSignature;
|
|
582
|
+
if (model.provider === "opencode-go" && signature === "reasoning") signature = "reasoning_content";
|
|
583
|
+
if (signature && signature.length > 0) assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (toolCalls.length > 0) {
|
|
587
|
+
assistantMsg.tool_calls = toolCalls.map((toolCall) => ({
|
|
588
|
+
id: toolCall.id,
|
|
589
|
+
type: "function",
|
|
590
|
+
function: {
|
|
591
|
+
name: toolCall.name,
|
|
592
|
+
arguments: JSON.stringify(toolCall.arguments)
|
|
593
|
+
}
|
|
594
|
+
}));
|
|
595
|
+
const reasoningDetails = toolCalls.flatMap((toolCall) => {
|
|
596
|
+
const signature = toolCall.thoughtSignature;
|
|
597
|
+
if (!signature) return [];
|
|
598
|
+
try {
|
|
599
|
+
const parsed = JSON.parse(signature);
|
|
600
|
+
return parsed ? [parsed] : [];
|
|
601
|
+
} catch {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
if (reasoningDetails.length > 0) assistantMsg.reasoning_details = reasoningDetails;
|
|
606
|
+
}
|
|
607
|
+
if (compat.requiresReasoningContentOnAssistantMessages && model.reasoning && assistantMsg.reasoning_content === void 0) assistantMsg.reasoning_content = "";
|
|
608
|
+
const content = assistantMsg.content;
|
|
609
|
+
if (!(content !== null && content !== void 0 && content.length > 0) && !assistantMsg.tool_calls) continue;
|
|
610
|
+
params.push(assistantMsg);
|
|
611
|
+
} else if (msg.role === "toolResult") {
|
|
612
|
+
const imageContentParts = [];
|
|
613
|
+
let j = i;
|
|
614
|
+
while (j < transformedMessages.length) {
|
|
615
|
+
const toolMsg = transformedMessages.at(j);
|
|
616
|
+
if (toolMsg?.role !== "toolResult") break;
|
|
617
|
+
const textResult = extractToolResultText(toolMsg.content);
|
|
618
|
+
const mediaPlaceholder = describeToolResultMediaPlaceholder(toolMsg.content);
|
|
619
|
+
const images = toolMsg.content.filter(isImageWithMediaPayload);
|
|
620
|
+
const toolResultMsg = {
|
|
621
|
+
role: "tool",
|
|
622
|
+
content: textResult.trim() ? textResult : mediaPlaceholder ?? "(no output)",
|
|
623
|
+
tool_call_id: toolMsg.toolCallId
|
|
624
|
+
};
|
|
625
|
+
if (compat.requiresToolResultName && toolMsg.toolName) toolResultMsg.name = toolMsg.toolName;
|
|
626
|
+
params.push(toolResultMsg);
|
|
627
|
+
if (images.length > 0 && model.input.includes("image")) {
|
|
628
|
+
const boundedToolName = sanitizeSurrogates(truncateUtf16Safe(toolMsg.toolName ?? "", 64));
|
|
629
|
+
imageContentParts.push({
|
|
630
|
+
type: "text",
|
|
631
|
+
text: `Image(s) from tool result #${j - i + 1}${boundedToolName ? ` (${boundedToolName})` : ""}:`
|
|
632
|
+
});
|
|
633
|
+
for (const block of images) imageContentParts.push({
|
|
634
|
+
type: "image_url",
|
|
635
|
+
image_url: { url: `data:${block.mimeType};base64,${block.data}` }
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
j += 1;
|
|
639
|
+
}
|
|
640
|
+
i = j - 1;
|
|
641
|
+
if (imageContentParts.length > 0) {
|
|
642
|
+
if (compat.requiresAssistantAfterToolResult) params.push({
|
|
643
|
+
role: "assistant",
|
|
644
|
+
content: "I have processed the tool results."
|
|
645
|
+
});
|
|
646
|
+
params.push({
|
|
647
|
+
role: "user",
|
|
648
|
+
content: imageContentParts
|
|
649
|
+
});
|
|
650
|
+
lastRole = "user";
|
|
651
|
+
} else lastRole = "toolResult";
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
lastRole = msg.role;
|
|
655
|
+
}
|
|
656
|
+
if (relocatableSplit !== void 0 && systemParamIndex !== void 0) relocateNonBehavioralRegion({
|
|
657
|
+
params,
|
|
658
|
+
systemParamIndex,
|
|
659
|
+
split: relocatableSplit,
|
|
660
|
+
cacheOptOutIndexes: options.cacheOptOutIndexes
|
|
661
|
+
});
|
|
662
|
+
return params;
|
|
663
|
+
}
|
|
664
|
+
/** Commit relocation only after finding an emitted carrier. */
|
|
665
|
+
function relocateNonBehavioralRegion(args) {
|
|
666
|
+
const text = sanitizeSurrogates(stripSystemPromptCacheBoundary(args.split.relocatable));
|
|
667
|
+
for (let index = args.systemParamIndex + 1; index < args.params.length; index++) {
|
|
668
|
+
const param = args.params[index];
|
|
669
|
+
if (!param || param.role !== "user") continue;
|
|
670
|
+
if (typeof param.content === "string") param.content = `${param.content}\n\n${text}`;
|
|
671
|
+
else if (Array.isArray(param.content)) param.content = [...param.content, {
|
|
672
|
+
type: "text",
|
|
673
|
+
text
|
|
674
|
+
}];
|
|
675
|
+
else continue;
|
|
676
|
+
const systemParam = args.params[args.systemParamIndex];
|
|
677
|
+
if (systemParam) systemParam.content = sanitizeSurrogates(stripSystemPromptCacheBoundary(args.split.remainingPrompt));
|
|
678
|
+
args.cacheOptOutIndexes?.add(index);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region packages/ai/src/transports/openai-completions-policy.ts
|
|
684
|
+
function applyCompletionsReasoningAndRouting(params, model, reasoning, compat, mode) {
|
|
685
|
+
const direct = mode === "direct";
|
|
686
|
+
const nativeEffort = compat.thinkingFormat === "openrouter" ? void 0 : reasoning.effort;
|
|
687
|
+
const enabled = reasoning.thinkingEnabled ?? false;
|
|
688
|
+
if (model.reasoning) {
|
|
689
|
+
let allowScalarEffort = true;
|
|
690
|
+
if (direct && compat.thinkingFormat === "zai") {
|
|
691
|
+
params.thinking = enabled ? {
|
|
692
|
+
type: "enabled",
|
|
693
|
+
clear_thinking: false
|
|
694
|
+
} : { type: "disabled" };
|
|
695
|
+
allowScalarEffort = false;
|
|
696
|
+
} else if (compat.thinkingFormat === "qwen") {
|
|
697
|
+
params.enable_thinking = enabled;
|
|
698
|
+
allowScalarEffort = false;
|
|
699
|
+
} else if (compat.thinkingFormat === "qwen-chat-template") {
|
|
700
|
+
params.chat_template_kwargs = {
|
|
701
|
+
enable_thinking: enabled,
|
|
702
|
+
...direct ? { preserve_thinking: true } : {}
|
|
703
|
+
};
|
|
704
|
+
allowScalarEffort = false;
|
|
705
|
+
} else if (compat.thinkingFormat === "together" || direct && compat.thinkingFormat === "deepseek") {
|
|
706
|
+
if (compat.thinkingFormat === "deepseek") params.thinking = { type: enabled ? "enabled" : "disabled" };
|
|
707
|
+
else params.reasoning = { enabled };
|
|
708
|
+
allowScalarEffort = enabled;
|
|
709
|
+
}
|
|
710
|
+
if (allowScalarEffort && compat.supportsReasoningEffort && (direct ? nativeEffort !== void 0 : nativeEffort)) params.reasoning_effort = nativeEffort;
|
|
711
|
+
}
|
|
712
|
+
if (!direct) return;
|
|
713
|
+
if (compat.openRouterRouting) params.provider = compat.openRouterRouting;
|
|
714
|
+
if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
|
|
715
|
+
const routing = model.compat.vercelGatewayRouting;
|
|
716
|
+
if (routing.only || routing.order) {
|
|
717
|
+
const gatewayOptions = {};
|
|
718
|
+
if (routing.only) gatewayOptions.only = routing.only;
|
|
719
|
+
if (routing.order) gatewayOptions.order = routing.order;
|
|
720
|
+
params.providerOptions = { gateway: gatewayOptions };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region packages/ai/src/transports/openai-completions-replay.ts
|
|
726
|
+
function isGoogleOpenAICompatModel(model) {
|
|
727
|
+
const endpointClass = detectOpenAICompletionsCompat(model).capabilities.endpointClass;
|
|
728
|
+
return model.provider === "google" || endpointClass === "google-generative-ai" || endpointClass === "google-vertex";
|
|
729
|
+
}
|
|
730
|
+
const GOOGLE_COMPAT_THOUGHT_SIGNATURE_ELLIPSIS_RE = /[\u2026]|\.\.\./;
|
|
731
|
+
const GOOGLE_COMPAT_THOUGHT_SIGNATURE_BASE64_RE = /^[A-Za-z0-9+/=]+$/;
|
|
732
|
+
function hasGoogleCompatThoughtSignatureTruncationFootprint(value) {
|
|
733
|
+
return GOOGLE_COMPAT_THOUGHT_SIGNATURE_ELLIPSIS_RE.test(value) || GOOGLE_COMPAT_THOUGHT_SIGNATURE_BASE64_RE.test(value) && value.length % 4 !== 0;
|
|
734
|
+
}
|
|
735
|
+
function injectToolCallThoughtSignatures(outgoingMessages, context, model) {
|
|
736
|
+
if (!isGoogleOpenAICompatModel(model)) return;
|
|
737
|
+
const sigById = /* @__PURE__ */ new Map();
|
|
738
|
+
const fallbackSig = isGoogleGemini3ThinkingLevelModel(model.id) ? GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP : void 0;
|
|
739
|
+
for (const msg of context.messages ?? []) {
|
|
740
|
+
if (msg.role !== "assistant") continue;
|
|
741
|
+
if (!Array.isArray(msg.content)) continue;
|
|
742
|
+
for (const block of msg.content) {
|
|
743
|
+
if (block.type !== "toolCall") continue;
|
|
744
|
+
const id = block.id;
|
|
745
|
+
const sig = block.thoughtSignature;
|
|
746
|
+
if (typeof id === "string" && typeof sig === "string" && sig.length > 0) {
|
|
747
|
+
const isSameRoute = msg.api === model.api && msg.provider === model.provider && msg.model === model.id;
|
|
748
|
+
if (!isSameRoute && !fallbackSig) continue;
|
|
749
|
+
sigById.set(id, isSameRoute ? sig : fallbackSig ?? sig);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (sigById.size === 0 && !fallbackSig) return;
|
|
754
|
+
for (const message of outgoingMessages) {
|
|
755
|
+
const toolCalls = "tool_calls" in message ? message.tool_calls : void 0;
|
|
756
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
757
|
+
for (const toolCall of toolCalls) {
|
|
758
|
+
let sig = sigById.get(toolCall.id) ?? fallbackSig;
|
|
759
|
+
if (sig) {
|
|
760
|
+
if (hasGoogleCompatThoughtSignatureTruncationFootprint(sig.trim())) sig = fallbackSig;
|
|
761
|
+
}
|
|
762
|
+
if (!sig) continue;
|
|
763
|
+
Object.assign(toolCall, { extra_content: { google: { thought_signature: sig } } });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const COMPLETIONS_REASONING_REPLAY_FIELDS = [
|
|
768
|
+
"reasoning_details",
|
|
769
|
+
"reasoning_content",
|
|
770
|
+
"reasoning",
|
|
771
|
+
"reasoning_text"
|
|
772
|
+
];
|
|
773
|
+
function sanitizeOpenRouterReasoningReplayFields(record) {
|
|
774
|
+
const reasoningDetails = record.reasoning_details;
|
|
775
|
+
if (typeof reasoningDetails === "string") {
|
|
776
|
+
if (reasoningDetails.length > 0 && typeof record.reasoning !== "string") record.reasoning = reasoningDetails;
|
|
777
|
+
delete record.reasoning_details;
|
|
778
|
+
} else if (reasoningDetails !== void 0 && !Array.isArray(reasoningDetails)) delete record.reasoning_details;
|
|
779
|
+
for (const field of ["reasoning", "reasoning_content"]) if (field in record && (typeof record[field] !== "string" || record[field] === "")) delete record[field];
|
|
780
|
+
const reasoningText = record.reasoning_text;
|
|
781
|
+
if (typeof reasoningText === "string" && reasoningText.length > 0 && typeof record.reasoning !== "string" && typeof record.reasoning_content !== "string") record.reasoning = reasoningText;
|
|
782
|
+
delete record.reasoning_text;
|
|
783
|
+
}
|
|
784
|
+
const REASONING_CONTENT_REPLAY_MODEL_IDS = /* @__PURE__ */ new Set([
|
|
785
|
+
"deepseek-v4-flash",
|
|
786
|
+
"deepseek-v4-pro",
|
|
787
|
+
"kimi-for-coding",
|
|
788
|
+
"kimi-k2.5",
|
|
789
|
+
"kimi-k2.6",
|
|
790
|
+
"kimi-k2.7-code",
|
|
791
|
+
"kimi-k2.7-code-highspeed",
|
|
792
|
+
"kimi-k3",
|
|
793
|
+
"kimi-k2-thinking",
|
|
794
|
+
"kimi-k2-thinking-turbo",
|
|
795
|
+
"mimo-v2-pro",
|
|
796
|
+
"mimo-v2-omni",
|
|
797
|
+
"mimo-v2.5",
|
|
798
|
+
"mimo-v2.5-pro",
|
|
799
|
+
"mimo-v2.6-flash",
|
|
800
|
+
"mimo-v2.6-pro",
|
|
801
|
+
"mimo-v2.6-pro-ultraspeed"
|
|
802
|
+
]);
|
|
803
|
+
const REASONING_CONTENT_REPLAY_TIER_SUFFIXES = [
|
|
804
|
+
"-free",
|
|
805
|
+
"-paid",
|
|
806
|
+
"-trial"
|
|
807
|
+
];
|
|
808
|
+
function stripReasoningContentReplayTierSuffix(modelId) {
|
|
809
|
+
for (const suffix of REASONING_CONTENT_REPLAY_TIER_SUFFIXES) if (modelId.length > suffix.length && modelId.endsWith(suffix)) return modelId.slice(0, -suffix.length);
|
|
810
|
+
return modelId;
|
|
811
|
+
}
|
|
812
|
+
function getReasoningContentReplayModelIdCandidates(modelId) {
|
|
813
|
+
if (typeof modelId !== "string") return [];
|
|
814
|
+
const normalized = modelId.trim().toLowerCase();
|
|
815
|
+
if (!normalized) return [];
|
|
816
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
817
|
+
const finalPart = parts[parts.length - 1] ?? normalized;
|
|
818
|
+
const candidates = [finalPart];
|
|
819
|
+
const colonParts = finalPart.split(":").filter(Boolean);
|
|
820
|
+
if (colonParts.length > 1) candidates.push(colonParts[0] ?? "", colonParts[colonParts.length - 1] ?? "");
|
|
821
|
+
for (const candidate of candidates.slice()) {
|
|
822
|
+
const stripped = stripReasoningContentReplayTierSuffix(candidate);
|
|
823
|
+
if (stripped !== candidate) candidates.push(stripped);
|
|
824
|
+
}
|
|
825
|
+
return uniqueStrings(candidates.filter(Boolean));
|
|
826
|
+
}
|
|
827
|
+
function shouldPreserveReasoningContentReplay(model, compat) {
|
|
828
|
+
if (compat.requiresReasoningContentOnAssistantMessages || compat.thinkingFormat === "deepseek" || compat.thinkingFormat === "zai" || shouldTrustReasoningContentReplayMetadata(model)) return true;
|
|
829
|
+
return getReasoningContentReplayModelIdCandidates(model.id).some((modelId) => REASONING_CONTENT_REPLAY_MODEL_IDS.has(modelId));
|
|
830
|
+
}
|
|
831
|
+
function shouldPreserveOpenRouterReasoningReplay(model) {
|
|
832
|
+
if (model.provider !== "openrouter") return true;
|
|
833
|
+
const normalizedModelId = model.id.trim().toLowerCase();
|
|
834
|
+
return !(normalizedModelId.startsWith("anthropic/") || normalizedModelId.startsWith("x-ai/"));
|
|
835
|
+
}
|
|
836
|
+
function shouldTrustReasoningContentReplayMetadata(model) {
|
|
837
|
+
if (!model.reasoning) return false;
|
|
838
|
+
if (model.provider.trim().toLowerCase() === "openai") return false;
|
|
839
|
+
return shouldPreserveOpenRouterReasoningReplay(model);
|
|
840
|
+
}
|
|
841
|
+
function applyCompletionsReplay(outgoingMessages, context, model, compat) {
|
|
842
|
+
injectToolCallThoughtSignatures(outgoingMessages, context, model);
|
|
843
|
+
const preserveOpenRouterReasoning = compat.thinkingFormat === "openrouter" && shouldPreserveOpenRouterReasoningReplay(model);
|
|
844
|
+
const preserveReasoningContent = shouldPreserveReasoningContentReplay(model, compat);
|
|
845
|
+
for (const message of outgoingMessages) {
|
|
846
|
+
if (message.role !== "assistant") continue;
|
|
847
|
+
if (preserveOpenRouterReasoning) sanitizeOpenRouterReasoningReplayFields(message);
|
|
848
|
+
else {
|
|
849
|
+
const record = message;
|
|
850
|
+
for (const field of COMPLETIONS_REASONING_REPLAY_FIELDS) {
|
|
851
|
+
if (preserveReasoningContent && field === "reasoning_content" && typeof record[field] === "string") continue;
|
|
852
|
+
delete record[field];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region packages/ai/src/transports/openai-completions-params.ts
|
|
859
|
+
function isKnownOpenAICompletionsEndpoint(model) {
|
|
860
|
+
if (!model.baseUrl.trim() || isNativeOpenAIEndpoint(model)) return true;
|
|
861
|
+
const endpointClass = resolveProviderEndpoint(model).endpointClass;
|
|
862
|
+
if (endpointClass === "openai-public" || endpointClass === "azure-openai") return true;
|
|
863
|
+
const endpoint = URL.parse(model.baseUrl);
|
|
864
|
+
return endpoint !== null && isAzureOpenAICompatibleHost(endpoint.hostname.toLowerCase());
|
|
865
|
+
}
|
|
866
|
+
function resolveOpenAICompletionsMaxTokens(model, options) {
|
|
867
|
+
if (options?.maxTokens) return {
|
|
868
|
+
maxTokens: options.maxTokens,
|
|
869
|
+
clampToModelMaxTokens: true
|
|
870
|
+
};
|
|
871
|
+
const paramsMaxTokens = resolveMaxTokensParam(model.params);
|
|
872
|
+
if (paramsMaxTokens) return {
|
|
873
|
+
maxTokens: paramsMaxTokens,
|
|
874
|
+
clampToModelMaxTokens: false
|
|
875
|
+
};
|
|
876
|
+
return {
|
|
877
|
+
maxTokens: model.maxTokens,
|
|
878
|
+
clampToModelMaxTokens: false
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function resolveOpenAICompletionsModelMaxTokens(model) {
|
|
882
|
+
const maxTokens = asPositiveFiniteNumber(model.maxTokens);
|
|
883
|
+
return maxTokens === void 0 ? void 0 : Math.floor(maxTokens);
|
|
884
|
+
}
|
|
885
|
+
const OPENAI_COMPLETIONS_INPUT_TOKEN_SAFETY_MARGIN = 1.25;
|
|
886
|
+
const OPENAI_COMPLETIONS_IMAGE_CHAR_ESTIMATE = 8e3;
|
|
887
|
+
const MIN_USEFUL_OUTPUT_TOKENS = 16;
|
|
888
|
+
function estimateJsonChars(value, fallback) {
|
|
889
|
+
try {
|
|
890
|
+
return estimateStringChars(JSON.stringify(value));
|
|
891
|
+
} catch {
|
|
892
|
+
return fallback;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function estimateOpenAICompletionsInputTokens(payload) {
|
|
896
|
+
let adjustedChars = 0;
|
|
897
|
+
adjustedChars += estimateOpenAICompletionsMessagesChars(payload.messages);
|
|
898
|
+
if (payload.tools?.length) adjustedChars += estimateJsonChars(payload.tools, 1024);
|
|
899
|
+
if (payload.response_format !== void 0) adjustedChars += estimateJsonChars(payload.response_format, 256);
|
|
900
|
+
return Math.ceil(adjustedChars / 4 * OPENAI_COMPLETIONS_INPUT_TOKEN_SAFETY_MARGIN);
|
|
901
|
+
}
|
|
902
|
+
function estimateOpenAICompletionsMessagesChars(messages) {
|
|
903
|
+
let adjustedChars = 0;
|
|
904
|
+
for (const message of messages) {
|
|
905
|
+
if (!message || typeof message !== "object") continue;
|
|
906
|
+
const record = message;
|
|
907
|
+
adjustedChars += estimateOpenAICompletionsContentChars(record.content);
|
|
908
|
+
for (const field of COMPLETIONS_REASONING_REPLAY_FIELDS) adjustedChars += estimateOpenAICompletionsContentChars(record[field]);
|
|
909
|
+
if (record.tool_calls !== void 0) adjustedChars += estimateJsonChars(record.tool_calls, 256);
|
|
910
|
+
}
|
|
911
|
+
return adjustedChars;
|
|
912
|
+
}
|
|
913
|
+
function estimateOpenAICompletionsContentChars(value) {
|
|
914
|
+
if (typeof value === "string") return estimateStringChars(value);
|
|
915
|
+
if (!Array.isArray(value)) return 0;
|
|
916
|
+
let adjustedChars = 0;
|
|
917
|
+
for (const block of value) {
|
|
918
|
+
if (!block || typeof block !== "object") continue;
|
|
919
|
+
const record = block;
|
|
920
|
+
if (record.type === "image_url" || record.type === "input_image") {
|
|
921
|
+
adjustedChars += OPENAI_COMPLETIONS_IMAGE_CHAR_ESTIMATE;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const text = record.text;
|
|
925
|
+
if (typeof text === "string") {
|
|
926
|
+
adjustedChars += estimateStringChars(text);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
adjustedChars += estimateJsonChars(block, 256);
|
|
930
|
+
}
|
|
931
|
+
return adjustedChars;
|
|
932
|
+
}
|
|
933
|
+
function convertTools(tools, compat, model, mode) {
|
|
934
|
+
const prepared = mode === "managed" ? prepareOpenAITools(tools) : void 0;
|
|
935
|
+
const projection = prepared?.projection ?? projectOpenAITools(tools);
|
|
936
|
+
const convert = () => {
|
|
937
|
+
const strict = mode === "direct" ? compat.supportsStrictMode ? false : void 0 : resolveOpenAIStrictToolFlagWithDiagnostics(projection, getAiTransportHost().resolveOpenAIStrictToolSetting(model, {
|
|
938
|
+
transport: "stream",
|
|
939
|
+
supportsStrictMode: compat?.supportsStrictMode
|
|
940
|
+
}), {
|
|
941
|
+
transport: "completions",
|
|
942
|
+
model
|
|
943
|
+
});
|
|
944
|
+
return {
|
|
945
|
+
projection,
|
|
946
|
+
tools: sortPromptCacheToolsByName(projection.tools).map((tool) => {
|
|
947
|
+
const functionTool = {
|
|
948
|
+
name: tool.name,
|
|
949
|
+
description: tool.description,
|
|
950
|
+
parameters: mode === "direct" ? tool.parameters : normalizeOpenAIStrictToolParameters(tool.parameters, strict === true, model.compat)
|
|
951
|
+
};
|
|
952
|
+
if (strict !== void 0) functionTool.strict = strict;
|
|
953
|
+
return {
|
|
954
|
+
type: "function",
|
|
955
|
+
function: functionTool
|
|
956
|
+
};
|
|
957
|
+
})
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
return prepared ? withPreparedToolSchemaNormalization(prepared.schemas, convert) : convert();
|
|
961
|
+
}
|
|
962
|
+
function buildOpenAICompletionsParams(model, context, options) {
|
|
963
|
+
return buildOpenAICompletionsRequest(model, context, options, { mode: "managed" });
|
|
964
|
+
}
|
|
965
|
+
function buildOpenAICompletionsRequest(model, context, options, policy) {
|
|
966
|
+
const resolvedPolicy = policy.mode === "direct" ? policy : {
|
|
967
|
+
...policy,
|
|
968
|
+
compat: getCompat(model)
|
|
969
|
+
};
|
|
970
|
+
const compat = resolvedPolicy.compat;
|
|
971
|
+
const managedCompat = resolvedPolicy.mode === "managed" ? resolvedPolicy.compat : void 0;
|
|
972
|
+
const endpointDetection = detectOpenAICompletionsCompat(model);
|
|
973
|
+
const compatDetection = policy.mode === "managed" ? endpointDetection : void 0;
|
|
974
|
+
const { endpointClass } = endpointDetection.capabilities;
|
|
975
|
+
const cacheRetention = policy.mode === "direct" ? policy.cacheRetention : resolveCacheRetention(options?.cacheRetention);
|
|
976
|
+
const cacheControl = resolveCompletionsCacheControl(compat, cacheRetention, endpointClass === "openrouter" || endpointClass === "default" && model.provider === "openrouter");
|
|
977
|
+
const markTools = endpointClass !== "modelstudio-native" && !(endpointClass === "default" && [
|
|
978
|
+
"modelstudio",
|
|
979
|
+
"dashscope",
|
|
980
|
+
"qwen"
|
|
981
|
+
].includes(model.provider));
|
|
982
|
+
const cacheOptOutIndexes = /* @__PURE__ */ new Set();
|
|
983
|
+
const convertedMessages = convertMessages(model, context, compat, {
|
|
984
|
+
cacheOptOutIndexes,
|
|
985
|
+
preserveSystemPromptCacheBoundary: cacheControl !== void 0 && !managedCompat?.requiresStringContent
|
|
986
|
+
});
|
|
987
|
+
let messages = convertedMessages;
|
|
988
|
+
if (managedCompat) {
|
|
989
|
+
applyCompletionsReplay(convertedMessages, context, model, managedCompat);
|
|
990
|
+
if (managedCompat.strictMessageKeys) messages = stripCompletionMessagesToRoleContent(messages);
|
|
991
|
+
if (managedCompat.requiresStringContent) messages = flattenCompletionMessagesToStringContent(messages);
|
|
992
|
+
}
|
|
993
|
+
const promptCacheKey = resolvePromptCacheKey(options, cacheRetention);
|
|
994
|
+
const params = {
|
|
995
|
+
model: model.id,
|
|
996
|
+
messages,
|
|
997
|
+
stream: (options?.streaming ?? model.params?.streaming) !== false,
|
|
998
|
+
...resolveOpenAIPromptCacheParams(model, cacheRetention, compat)
|
|
999
|
+
};
|
|
1000
|
+
if (params.stream && compat.supportsUsageInStreaming) params.stream_options = { include_usage: true };
|
|
1001
|
+
if (compat.supportsStore) params.store = false;
|
|
1002
|
+
if (policy.mode === "direct" || compat.supportsPromptCacheKey && promptCacheKey) params.prompt_cache_key = compat.supportsPromptCacheKey ? promptCacheKey : void 0;
|
|
1003
|
+
if (options?.temperature !== void 0) params.temperature = options.temperature;
|
|
1004
|
+
if (policy.mode === "managed" && options?.topP !== void 0) params.top_p = options.topP;
|
|
1005
|
+
const requestedResponseFormat = options?.responseFormat;
|
|
1006
|
+
const responseFormat = policy.mode === "direct" && requestedResponseFormat === void 0 ? void 0 : resolveOpenAICompletionsResponseFormat(shouldOmitOllamaCompatResponseFormat({
|
|
1007
|
+
provider: model.provider,
|
|
1008
|
+
baseUrl: model.baseUrl,
|
|
1009
|
+
hasTools: () => Boolean(context.tools?.length)
|
|
1010
|
+
}) ? void 0 : requestedResponseFormat, compat.supportsJsonSchemaResponseFormat);
|
|
1011
|
+
if (responseFormat !== void 0) params.response_format = responseFormat;
|
|
1012
|
+
if (policy.mode === "managed" && options?.frequencyPenalty !== void 0) params.frequency_penalty = options.frequencyPenalty;
|
|
1013
|
+
if (policy.mode === "managed" && options?.presencePenalty !== void 0) params.presence_penalty = options.presencePenalty;
|
|
1014
|
+
if (policy.mode === "managed" && options?.seed !== void 0) params.seed = options.seed;
|
|
1015
|
+
if (options?.stop !== void 0 && options.stop.length > 0) params.stop = options.stop;
|
|
1016
|
+
let directToolProjection;
|
|
1017
|
+
if (policy.mode === "direct" || supportsModelTools(model)) {
|
|
1018
|
+
if (context.tools) {
|
|
1019
|
+
const converted = convertTools(context.tools, compat, model, policy.mode);
|
|
1020
|
+
if (policy.mode === "direct") directToolProjection = converted.projection;
|
|
1021
|
+
if (converted.tools.length > 0 || policy.mode === "managed" && converted.projection.inputToolCount === 0 && converted.projection.diagnostics.length === 0) params.tools = converted.tools;
|
|
1022
|
+
else if (hasToolCallHistory(context.messages)) params.tools = [];
|
|
1023
|
+
if (policy.mode === "direct" && params.stream && compat.zaiToolStream && converted.tools.length > 0) params.tool_stream = true;
|
|
1024
|
+
if (policy.mode === "managed" && options?.toolChoice) {
|
|
1025
|
+
const toolChoice = reconcileOpenAICompletionsToolChoice(options.toolChoice, converted.projection);
|
|
1026
|
+
if (toolChoice !== void 0) params.tool_choice = toolChoice;
|
|
1027
|
+
} else if (compatDetection?.capabilities.usesExplicitProxyLikeEndpoint && params.tools?.length) params.tool_choice = "auto";
|
|
1028
|
+
} else if (hasToolCallHistory(context.messages)) params.tools = [];
|
|
1029
|
+
if (compatDetection?.capabilities.usesExplicitProxyLikeEndpoint && params.tools?.length === 0) {
|
|
1030
|
+
delete params.tools;
|
|
1031
|
+
delete params.tool_choice;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (policy.mode === "direct" && compat.cacheControlFormat === "anthropic") applyCompletionsAnthropicCacheControl(params, cacheControl ?? null, cacheOptOutIndexes, markTools);
|
|
1035
|
+
if (policy.mode === "direct" && options?.toolChoice) {
|
|
1036
|
+
const toolChoice = reconcileOpenAICompletionsToolChoice(options.toolChoice, directToolProjection ?? projectOpenAITools([]));
|
|
1037
|
+
if (toolChoice !== void 0) params.tool_choice = toolChoice;
|
|
1038
|
+
}
|
|
1039
|
+
const isOpenRouter = compat.thinkingFormat === "openrouter";
|
|
1040
|
+
const usesBinaryOpenRouterThinking = isOpenRouter && (model.compat?.supportsReasoningEffort === false || model.compat?.supportedReasoningEfforts?.length === 0);
|
|
1041
|
+
const simpleReasoning = options?.reasoning;
|
|
1042
|
+
const requestedEffort = policy.mode === "direct" ? options?.reasoningEffort : options?.reasoningEffort ?? (simpleReasoning === "none" ? "none" : resolveOpenAISimpleReasoningEffort({
|
|
1043
|
+
...model,
|
|
1044
|
+
compat: model.compat ?? void 0
|
|
1045
|
+
}, simpleReasoning)) ?? (usesBinaryOpenRouterThinking ? void 0 : "high");
|
|
1046
|
+
const reasoning = resolveOpenAIRequestReasoning(model, requestedEffort, compat.reasoningEffortForOff);
|
|
1047
|
+
const { effort, thinkingEnabled } = reasoning;
|
|
1048
|
+
{
|
|
1049
|
+
const maxTokenBudget = policy.mode === "direct" ? {
|
|
1050
|
+
maxTokens: options?.maxTokens,
|
|
1051
|
+
clampToModelMaxTokens: true
|
|
1052
|
+
} : resolveOpenAICompletionsMaxTokens(model, options);
|
|
1053
|
+
const effectiveMaxTokens = maxTokenBudget.maxTokens;
|
|
1054
|
+
const effectiveContextTokens = asPositiveFiniteNumber(model.contextTokens) ?? asPositiveFiniteNumber(model.contextWindow);
|
|
1055
|
+
let clampedMaxTokens = effectiveMaxTokens;
|
|
1056
|
+
const modelMaxTokens = resolveOpenAICompletionsModelMaxTokens(model);
|
|
1057
|
+
if (maxTokenBudget.clampToModelMaxTokens && clampedMaxTokens !== void 0 && modelMaxTokens !== void 0 && clampedMaxTokens > modelMaxTokens) {
|
|
1058
|
+
clampedMaxTokens = modelMaxTokens;
|
|
1059
|
+
if (policy.mode === "managed") emitModelTransportDebug(log, `[completions] clamp_max_tokens provider=${model.provider} api=${model.api} model=${model.id} requested=${effectiveMaxTokens} output=${clampedMaxTokens} modelMaxTokens=${modelMaxTokens}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (compatDetection?.capabilities.usesExplicitProxyLikeEndpoint && clampedMaxTokens !== void 0 && effectiveContextTokens !== void 0) {
|
|
1062
|
+
const estimatedInputTokens = estimateOpenAICompletionsInputTokens(params);
|
|
1063
|
+
const remainingBudget = Math.max(0, effectiveContextTokens - estimatedInputTokens - 1);
|
|
1064
|
+
if (clampedMaxTokens > remainingBudget) {
|
|
1065
|
+
if (remainingBudget < MIN_USEFUL_OUTPUT_TOKENS) throw Object.assign(/* @__PURE__ */ new Error(`Context window exceeded: estimated input ${estimatedInputTokens} leaves only ${remainingBudget} output tokens within the ${effectiveContextTokens}-token context.`), { code: "context_length_exceeded" });
|
|
1066
|
+
clampedMaxTokens = remainingBudget;
|
|
1067
|
+
emitModelTransportDebug(log, `[completions] clamp_max_tokens provider=${model.provider} api=${model.api} model=${model.id} requested=${effectiveMaxTokens} output=${clampedMaxTokens} effectiveContext=${effectiveContextTokens} estimatedInput=${estimatedInputTokens}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (policy.mode === "direct" ? options?.maxTokens : clampedMaxTokens) {
|
|
1071
|
+
if (compat.maxTokensField === "max_tokens") params.max_tokens = clampedMaxTokens;
|
|
1072
|
+
else params.max_completion_tokens = clampedMaxTokens;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (isOpenRouter && model.reasoning) {
|
|
1076
|
+
if (usesBinaryOpenRouterThinking && thinkingEnabled !== void 0) params.reasoning = { enabled: thinkingEnabled };
|
|
1077
|
+
else if (effort !== void 0) params.reasoning = { effort };
|
|
1078
|
+
}
|
|
1079
|
+
applyCompletionsReasoningAndRouting(params, model, reasoning, compat, policy.mode);
|
|
1080
|
+
if (policy.mode === "managed" && compat.cacheControlFormat === "anthropic") applyCompletionsAnthropicCacheControl(params, cacheControl ?? null, cacheOptOutIndexes, markTools, !managedCompat?.requiresStringContent);
|
|
1081
|
+
if (params.tools?.length && isKnownOpenAICompletionsEndpoint(model)) {
|
|
1082
|
+
if (isOpenAIGpt56Model(model)) params.reasoning_effort = "none";
|
|
1083
|
+
else if (isOpenAIGpt54MiniModel(model) || isOpenAIGpt55Model(model)) delete params.reasoning_effort;
|
|
1084
|
+
}
|
|
1085
|
+
return params;
|
|
1086
|
+
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region packages/ai/src/transports/openai-completions-dsml.ts
|
|
1089
|
+
const DEEPSEEK_DSML_TOOL_KINDS = [
|
|
1090
|
+
"tool_calls",
|
|
1091
|
+
"tool_call",
|
|
1092
|
+
"function_calls"
|
|
1093
|
+
];
|
|
1094
|
+
const DEEPSEEK_DSML_TOOL_OPEN_TOKENS = DEEPSEEK_DSML_MARKERS.flatMap((marker) => DEEPSEEK_DSML_TOOL_KINDS.map((kind) => `<${marker}${kind}>`));
|
|
1095
|
+
const DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES = DEEPSEEK_DSML_MARKERS.map((marker) => `<${marker}invoke`);
|
|
1096
|
+
const DEEPSEEK_DSML_INVOKE_OPEN_TAG = new RegExp(`^<${DEEPSEEK_DSML_MARKER_PATTERN}invoke\\b[^<>]*>$`);
|
|
1097
|
+
const DEEPSEEK_DSML_TOOL_MAX_OPEN_TOKEN_LEN = Math.max(...DEEPSEEK_DSML_TOOL_OPEN_TOKENS.map((token) => token.length));
|
|
1098
|
+
const DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN = Math.max(...DEEPSEEK_DSML_TOOL_OPEN_TOKENS.map((token) => token.length + 1), ...DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES.map((token) => token.length + 2));
|
|
1099
|
+
const MAX_DSML_RECOVERY_BUFFER_BYTES = 256e3;
|
|
1100
|
+
const DEEPSEEK_DSML_SCAN_BATCH_CHARS = 65536;
|
|
1101
|
+
const DSML_NAME_ATTRIBUTE_RE = /\bname=("([^"]*)"|'([^']*)'|([^\s>]+))/;
|
|
1102
|
+
function createDsmlRecoverer() {
|
|
1103
|
+
let buffer = "";
|
|
1104
|
+
let bufferBytes = 0;
|
|
1105
|
+
let bufferEndsWithHighSurrogate = false;
|
|
1106
|
+
let pendingScanChars = 0;
|
|
1107
|
+
let activeOpenToken = null;
|
|
1108
|
+
let blockScanState = { offset: 0 };
|
|
1109
|
+
const resetBlockScan = () => {
|
|
1110
|
+
activeOpenToken = null;
|
|
1111
|
+
pendingScanChars = 0;
|
|
1112
|
+
blockScanState = { offset: 0 };
|
|
1113
|
+
};
|
|
1114
|
+
const consume = (final) => {
|
|
1115
|
+
const output = [];
|
|
1116
|
+
while (buffer) {
|
|
1117
|
+
const open = activeOpenToken ? {
|
|
1118
|
+
index: 0,
|
|
1119
|
+
token: activeOpenToken
|
|
1120
|
+
} : findEarliestDsmlToken(buffer, DEEPSEEK_DSML_TOOL_OPEN_TOKENS);
|
|
1121
|
+
if (!open) {
|
|
1122
|
+
resetBlockScan();
|
|
1123
|
+
if (final) {
|
|
1124
|
+
output.push({
|
|
1125
|
+
kind: "text",
|
|
1126
|
+
text: buffer
|
|
1127
|
+
});
|
|
1128
|
+
buffer = "";
|
|
1129
|
+
bufferBytes = 0;
|
|
1130
|
+
bufferEndsWithHighSurrogate = false;
|
|
1131
|
+
return output;
|
|
1132
|
+
}
|
|
1133
|
+
const keep = longestDsmlTokenPrefixSuffixLength(buffer, DEEPSEEK_DSML_TOOL_OPEN_TOKENS, DEEPSEEK_DSML_TOOL_MAX_OPEN_TOKEN_LEN);
|
|
1134
|
+
const emitLength = buffer.length - keep;
|
|
1135
|
+
if (emitLength > 0) {
|
|
1136
|
+
const emitted = buffer.slice(0, emitLength);
|
|
1137
|
+
output.push({
|
|
1138
|
+
kind: "text",
|
|
1139
|
+
text: emitted
|
|
1140
|
+
});
|
|
1141
|
+
bufferBytes -= Buffer.byteLength(emitted, "utf8");
|
|
1142
|
+
buffer = buffer.slice(emitted.length);
|
|
1143
|
+
if (!buffer) bufferEndsWithHighSurrogate = false;
|
|
1144
|
+
}
|
|
1145
|
+
return output;
|
|
1146
|
+
}
|
|
1147
|
+
if (open.index > 0) {
|
|
1148
|
+
const prefix = buffer.slice(0, open.index);
|
|
1149
|
+
output.push({
|
|
1150
|
+
kind: "text",
|
|
1151
|
+
text: prefix
|
|
1152
|
+
});
|
|
1153
|
+
bufferBytes -= Buffer.byteLength(prefix, "utf8");
|
|
1154
|
+
buffer = buffer.slice(prefix.length);
|
|
1155
|
+
resetBlockScan();
|
|
1156
|
+
}
|
|
1157
|
+
activeOpenToken = open.token;
|
|
1158
|
+
if (blockScanState.offset === 0) blockScanState.offset = open.token.length;
|
|
1159
|
+
const blockScan = scanDeepSeekDsmlToolBlock(buffer, open.token.replace("<", "</"), open.token.length, blockScanState);
|
|
1160
|
+
if (blockScan.kind === "nested-open") throw new Error("Nested DeepSeek DSML recovery wrappers are not supported");
|
|
1161
|
+
const close = blockScan.kind === "close" ? blockScan : null;
|
|
1162
|
+
if (!close) {
|
|
1163
|
+
if (final) {
|
|
1164
|
+
output.push({
|
|
1165
|
+
kind: "text",
|
|
1166
|
+
text: buffer
|
|
1167
|
+
});
|
|
1168
|
+
buffer = "";
|
|
1169
|
+
bufferBytes = 0;
|
|
1170
|
+
bufferEndsWithHighSurrogate = false;
|
|
1171
|
+
return output;
|
|
1172
|
+
}
|
|
1173
|
+
if (bufferBytes > MAX_DSML_RECOVERY_BUFFER_BYTES) throw new Error("Exceeded DeepSeek DSML recovery buffer limit");
|
|
1174
|
+
return output;
|
|
1175
|
+
}
|
|
1176
|
+
resetBlockScan();
|
|
1177
|
+
const body = buffer.slice(open.token.length, close.index);
|
|
1178
|
+
const blockText = buffer.slice(0, close.index + close.token.length);
|
|
1179
|
+
if (Buffer.byteLength(blockText, "utf8") > MAX_DSML_RECOVERY_BUFFER_BYTES) throw new Error("Exceeded DeepSeek DSML recovery buffer limit");
|
|
1180
|
+
const recoveredToolCalls = parseDeepSeekDsmlToolCallBlock(body);
|
|
1181
|
+
if (recoveredToolCalls.length > 0) output.push(...recoveredToolCalls);
|
|
1182
|
+
else output.push({
|
|
1183
|
+
kind: "text",
|
|
1184
|
+
text: blockText
|
|
1185
|
+
});
|
|
1186
|
+
bufferBytes -= Buffer.byteLength(blockText, "utf8");
|
|
1187
|
+
buffer = buffer.slice(blockText.length);
|
|
1188
|
+
if (!buffer) bufferEndsWithHighSurrogate = false;
|
|
1189
|
+
}
|
|
1190
|
+
return output;
|
|
1191
|
+
};
|
|
1192
|
+
return {
|
|
1193
|
+
push(chunk) {
|
|
1194
|
+
const append = measureUtf8AppendBytes(bufferEndsWithHighSurrogate, chunk);
|
|
1195
|
+
bufferBytes += append.bytes;
|
|
1196
|
+
bufferEndsWithHighSurrogate = append.endsWithHighSurrogate;
|
|
1197
|
+
buffer += chunk;
|
|
1198
|
+
pendingScanChars += chunk.length;
|
|
1199
|
+
if (activeOpenToken && pendingScanChars < DEEPSEEK_DSML_SCAN_BATCH_CHARS && !chunk.includes("<") && !chunk.includes(">") && bufferBytes <= MAX_DSML_RECOVERY_BUFFER_BYTES) return [];
|
|
1200
|
+
pendingScanChars = 0;
|
|
1201
|
+
return consume(false);
|
|
1202
|
+
},
|
|
1203
|
+
flush() {
|
|
1204
|
+
return consume(true);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
function parseDeepSeekDsmlToolCallBlock(body) {
|
|
1209
|
+
const toolCalls = [];
|
|
1210
|
+
const invokeOpenRegex = new RegExp(`<${DEEPSEEK_DSML_MARKER_PATTERN}invoke\\b([^<>]*)>`, "g");
|
|
1211
|
+
let openMatch;
|
|
1212
|
+
while ((openMatch = invokeOpenRegex.exec(body)) !== null) {
|
|
1213
|
+
const invokeBodyStart = openMatch.index + openMatch[0].length;
|
|
1214
|
+
const invokeCloseToken = `</${openMatch[1]}invoke>`;
|
|
1215
|
+
const invokeCloseIndex = body.indexOf(invokeCloseToken, invokeBodyStart);
|
|
1216
|
+
if (invokeCloseIndex === -1) break;
|
|
1217
|
+
const invokeBody = body.slice(invokeBodyStart, invokeCloseIndex);
|
|
1218
|
+
invokeOpenRegex.lastIndex = invokeCloseIndex + invokeCloseToken.length;
|
|
1219
|
+
const invokeName = parseDsmlNameAttribute(openMatch[2] ?? "");
|
|
1220
|
+
if (!invokeName) continue;
|
|
1221
|
+
const parsedArguments = parseDeepSeekDsmlInvokeArguments(invokeBody);
|
|
1222
|
+
if (!parsedArguments) continue;
|
|
1223
|
+
toolCalls.push({
|
|
1224
|
+
kind: "toolCall",
|
|
1225
|
+
name: invokeName,
|
|
1226
|
+
arguments: parsedArguments,
|
|
1227
|
+
partialArgs: JSON.stringify(parsedArguments)
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
return toolCalls;
|
|
1231
|
+
}
|
|
1232
|
+
function parseDeepSeekDsmlInvokeArguments(body) {
|
|
1233
|
+
const args = {};
|
|
1234
|
+
const parameterRegex = new RegExp(`<${DEEPSEEK_DSML_MARKER_PATTERN}parameter\\b([^>]*)>([\\s\\S]*?)</\\1parameter>`, "g");
|
|
1235
|
+
let parameterMatch;
|
|
1236
|
+
while ((parameterMatch = parameterRegex.exec(body)) !== null) {
|
|
1237
|
+
const name = parseDsmlNameAttribute(parameterMatch[2] ?? "");
|
|
1238
|
+
if (!name) continue;
|
|
1239
|
+
const rawValue = parameterMatch[3] ?? "";
|
|
1240
|
+
if (rawValue.length === 0) {
|
|
1241
|
+
const stringAttribute = Array.from((parameterMatch[2] ?? "").matchAll(/(?:^|\s)([^\s=]+)=(?:"([^"]*)"|'([^']*)'|([^\s"']+))(?=\s|$)/g)).find((attribute) => attribute[1] === "string");
|
|
1242
|
+
if ((stringAttribute?.[2] ?? stringAttribute?.[3] ?? stringAttribute?.[4]) !== "true") continue;
|
|
1243
|
+
}
|
|
1244
|
+
Object.defineProperty(args, name, {
|
|
1245
|
+
value: decodeDeepSeekDsmlText(rawValue),
|
|
1246
|
+
enumerable: true,
|
|
1247
|
+
configurable: true,
|
|
1248
|
+
writable: true
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
if (Object.keys(args).length > 0) return args;
|
|
1252
|
+
const trimmed = body.trim();
|
|
1253
|
+
if (!trimmed.startsWith("{")) return null;
|
|
1254
|
+
try {
|
|
1255
|
+
const parsed = JSON.parse(trimmed);
|
|
1256
|
+
if (isRecord(parsed) && Object.keys(parsed).length > 0) return parsed;
|
|
1257
|
+
} catch {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
function parseDsmlNameAttribute(attributes) {
|
|
1263
|
+
const match = DSML_NAME_ATTRIBUTE_RE.exec(attributes);
|
|
1264
|
+
const value = match?.[2] ?? match?.[3] ?? match?.[4];
|
|
1265
|
+
return value ? decodeDeepSeekDsmlText(value) : null;
|
|
1266
|
+
}
|
|
1267
|
+
function decodeDeepSeekDsmlText(value) {
|
|
1268
|
+
return value.replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
1269
|
+
}
|
|
1270
|
+
function scanDeepSeekDsmlToolBlock(text, closeToken, contentStartIndex, state) {
|
|
1271
|
+
while (state.offset < text.length) {
|
|
1272
|
+
if (state.invoke?.kind === "open") {
|
|
1273
|
+
const nextOpen = text.indexOf("<", state.offset);
|
|
1274
|
+
const nextClose = text.indexOf(">", state.offset);
|
|
1275
|
+
if (nextClose === -1 && nextOpen === -1) {
|
|
1276
|
+
state.offset = text.length;
|
|
1277
|
+
return { kind: "incomplete" };
|
|
1278
|
+
}
|
|
1279
|
+
if (nextOpen !== -1 && (nextClose === -1 || nextOpen < nextClose)) {
|
|
1280
|
+
state.invoke = void 0;
|
|
1281
|
+
state.offset = nextOpen;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
const invokeOpenTag = text.slice(state.invoke.start, nextClose + 1);
|
|
1285
|
+
const open = DEEPSEEK_DSML_INVOKE_OPEN_TAG.exec(invokeOpenTag);
|
|
1286
|
+
if (!open) {
|
|
1287
|
+
state.offset = state.invoke.start + 1;
|
|
1288
|
+
state.invoke = void 0;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
state.invoke = {
|
|
1292
|
+
kind: "body",
|
|
1293
|
+
closeToken: `</${open[1]}invoke>`
|
|
1294
|
+
};
|
|
1295
|
+
state.offset = nextClose + 1;
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (state.invoke?.kind === "body") {
|
|
1299
|
+
const invokeCloseIndex = text.indexOf(state.invoke.closeToken, state.offset);
|
|
1300
|
+
if (invokeCloseIndex === -1) {
|
|
1301
|
+
state.offset = Math.max(0, text.length - DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN + 1);
|
|
1302
|
+
return { kind: "incomplete" };
|
|
1303
|
+
}
|
|
1304
|
+
state.offset = invokeCloseIndex + state.invoke.closeToken.length;
|
|
1305
|
+
state.invoke = void 0;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const toolOpen = findEarliestDsmlToken(text, DEEPSEEK_DSML_TOOL_OPEN_TOKENS, state.offset);
|
|
1309
|
+
const toolCloseIndex = text.indexOf(closeToken, state.offset);
|
|
1310
|
+
const invokeOpen = findEarliestDsmlToken(text, DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES, state.offset);
|
|
1311
|
+
const next = [
|
|
1312
|
+
toolOpen ? {
|
|
1313
|
+
kind: "nested-open",
|
|
1314
|
+
...toolOpen
|
|
1315
|
+
} : null,
|
|
1316
|
+
toolCloseIndex === -1 ? null : {
|
|
1317
|
+
kind: "close",
|
|
1318
|
+
index: toolCloseIndex,
|
|
1319
|
+
token: closeToken
|
|
1320
|
+
},
|
|
1321
|
+
invokeOpen ? {
|
|
1322
|
+
kind: "invoke-open",
|
|
1323
|
+
...invokeOpen
|
|
1324
|
+
} : null
|
|
1325
|
+
].filter((candidate) => candidate !== null).toSorted((left, right) => left.index - right.index)[0];
|
|
1326
|
+
if (!next) {
|
|
1327
|
+
state.offset = Math.max(contentStartIndex, text.length - DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN + 1);
|
|
1328
|
+
return { kind: "incomplete" };
|
|
1329
|
+
}
|
|
1330
|
+
if (next.kind === "invoke-open") {
|
|
1331
|
+
state.invoke = {
|
|
1332
|
+
kind: "open",
|
|
1333
|
+
start: next.index
|
|
1334
|
+
};
|
|
1335
|
+
state.offset = next.index + next.token.length;
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
return next;
|
|
1339
|
+
}
|
|
1340
|
+
return { kind: "incomplete" };
|
|
1341
|
+
}
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region packages/ai/src/transports/openai-completions-gemma.ts
|
|
1344
|
+
const OPEN = "<|tool_call>";
|
|
1345
|
+
const CLOSE = "<tool_call|>";
|
|
1346
|
+
const QUOTE = "<|\"|>";
|
|
1347
|
+
const MAX_RECOVERY_BYTES = 256e3;
|
|
1348
|
+
function recoverGemmaCalls(text) {
|
|
1349
|
+
const parts = [];
|
|
1350
|
+
const opener = /\s*<\|tool_call>\s*call:([\w.-]+)\s*/y;
|
|
1351
|
+
const token = /\s*(<\|"\|>[\s\S]*?<\|"\|>|[\w.+-]+|[{}[\],:])\s*/y;
|
|
1352
|
+
let offset = 0;
|
|
1353
|
+
while (offset < text.length) {
|
|
1354
|
+
if (!text.slice(offset).trim()) {
|
|
1355
|
+
parts.push({
|
|
1356
|
+
kind: "text",
|
|
1357
|
+
text: text.slice(offset)
|
|
1358
|
+
});
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
opener.lastIndex = offset;
|
|
1362
|
+
const head = opener.exec(text);
|
|
1363
|
+
const name = head?.[1];
|
|
1364
|
+
if (!head || !name) return;
|
|
1365
|
+
const leading = /^\s*/.exec(head[0])?.[0] ?? "";
|
|
1366
|
+
if (leading) parts.push({
|
|
1367
|
+
kind: "text",
|
|
1368
|
+
text: leading
|
|
1369
|
+
});
|
|
1370
|
+
token.lastIndex = opener.lastIndex;
|
|
1371
|
+
let depth = 0;
|
|
1372
|
+
const json = [];
|
|
1373
|
+
do {
|
|
1374
|
+
const value = token.exec(text)?.[1];
|
|
1375
|
+
if (!value || json.length === 0 && value !== "{") return;
|
|
1376
|
+
if (value === "{") depth += 1;
|
|
1377
|
+
else if (value === "}") depth -= 1;
|
|
1378
|
+
json.push(value.startsWith(QUOTE) ? JSON.stringify(value.slice(5, -5)) : text[token.lastIndex] === ":" && /^[A-Za-z_][\w.-]*$/.test(value) ? JSON.stringify(value) : value);
|
|
1379
|
+
} while (depth > 0);
|
|
1380
|
+
if (!text.startsWith(CLOSE, token.lastIndex)) return;
|
|
1381
|
+
try {
|
|
1382
|
+
const argumentsValue = JSON.parse(json.join(" "));
|
|
1383
|
+
if (!isRecord(argumentsValue)) return;
|
|
1384
|
+
parts.push({
|
|
1385
|
+
kind: "toolCall",
|
|
1386
|
+
name,
|
|
1387
|
+
arguments: argumentsValue,
|
|
1388
|
+
partialArgs: JSON.stringify(argumentsValue)
|
|
1389
|
+
});
|
|
1390
|
+
} catch {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
offset = token.lastIndex + 12;
|
|
1394
|
+
}
|
|
1395
|
+
return parts;
|
|
1396
|
+
}
|
|
1397
|
+
/** Buffer only a possible standalone call batch; prose and examples stay text. */
|
|
1398
|
+
function createGemmaToolCallRecoverer() {
|
|
1399
|
+
let buffer = "";
|
|
1400
|
+
let bytes = 0;
|
|
1401
|
+
let highSurrogate = false;
|
|
1402
|
+
let passthrough = false;
|
|
1403
|
+
return {
|
|
1404
|
+
push(text) {
|
|
1405
|
+
if (passthrough) return [{
|
|
1406
|
+
kind: "text",
|
|
1407
|
+
text
|
|
1408
|
+
}];
|
|
1409
|
+
const append = measureUtf8AppendBytes(highSurrogate, text);
|
|
1410
|
+
bytes += append.bytes;
|
|
1411
|
+
highSurrogate = append.endsWithHighSurrogate;
|
|
1412
|
+
buffer += text;
|
|
1413
|
+
const candidate = buffer.trimStart();
|
|
1414
|
+
if (OPEN.startsWith(candidate) || candidate.startsWith(OPEN)) {
|
|
1415
|
+
if (bytes > MAX_RECOVERY_BYTES) throw new Error("Exceeded Gemma tool-call recovery buffer limit");
|
|
1416
|
+
return [];
|
|
1417
|
+
}
|
|
1418
|
+
passthrough = true;
|
|
1419
|
+
const result = [{
|
|
1420
|
+
kind: "text",
|
|
1421
|
+
text: buffer
|
|
1422
|
+
}];
|
|
1423
|
+
buffer = "";
|
|
1424
|
+
return result;
|
|
1425
|
+
},
|
|
1426
|
+
flush(allowRecovery = true) {
|
|
1427
|
+
const text = buffer;
|
|
1428
|
+
buffer = "";
|
|
1429
|
+
passthrough ||= !allowRecovery;
|
|
1430
|
+
const recovered = allowRecovery ? recoverGemmaCalls(text) : void 0;
|
|
1431
|
+
return text ? recovered ?? [{
|
|
1432
|
+
kind: "text",
|
|
1433
|
+
text
|
|
1434
|
+
}] : [];
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1439
|
+
//#region packages/ai/src/transports/openai-completions-stream.ts
|
|
1440
|
+
function extractToolCallThoughtSignature(toolCall) {
|
|
1441
|
+
const tc = toolCall;
|
|
1442
|
+
if (!tc) return;
|
|
1443
|
+
const extra = tc.extra_content?.google;
|
|
1444
|
+
return readNonEmptyStringPreservingWhitespace(extra?.thought_signature) ?? readNonEmptyStringPreservingWhitespace(tc.function?.thought_signature) ?? readNonEmptyStringPreservingWhitespace(tc.thought_signature);
|
|
1445
|
+
}
|
|
1446
|
+
async function processCompletionsStream(responseStream, output, model, stream, options) {
|
|
1447
|
+
const MAX_POST_TOOL_CALL_BUFFER_BYTES = 256e3;
|
|
1448
|
+
const directMode = options?.mode === "direct";
|
|
1449
|
+
const emitReasoning = options?.emitReasoning ?? true;
|
|
1450
|
+
const openAIModel = model;
|
|
1451
|
+
const compat = getCompat(openAIModel);
|
|
1452
|
+
const visibleReasoningDetailTypes = new Set(compat.visibleReasoningDetailTypes);
|
|
1453
|
+
const shouldFilterDeepSeekDsmlText = !directMode && compat.thinkingFormat === "deepseek";
|
|
1454
|
+
const deepSeekTextFilter = shouldFilterDeepSeekDsmlText ? createDeepSeekTextFilter() : null;
|
|
1455
|
+
const deepSeekToolCallRecoverer = shouldFilterDeepSeekDsmlText ? createDsmlRecoverer() : null;
|
|
1456
|
+
const gemmaToolCallRecoverer = !directMode && /gemma-?4/i.test(model.id) ? createGemmaToolCallRecoverer() : null;
|
|
1457
|
+
const reasoningTagTextPartitioner = createReasoningTagTextPartitioner();
|
|
1458
|
+
if (options?.strictReasoningTags) reasoningTagTextPartitioner.markStrict();
|
|
1459
|
+
let currentBlock = null;
|
|
1460
|
+
const directContent = { block: null };
|
|
1461
|
+
let currentTextSource;
|
|
1462
|
+
let pendingInterruptedTextBlock = null;
|
|
1463
|
+
let confirmedInterruptedTextBlock = null;
|
|
1464
|
+
let pendingPostToolCallDeltas = [];
|
|
1465
|
+
let pendingPostToolCallBytes = 0;
|
|
1466
|
+
const toolCallBlocksByIndex = /* @__PURE__ */ new Map();
|
|
1467
|
+
const toolCallBlocksById = /* @__PURE__ */ new Map();
|
|
1468
|
+
const encryptedReasoning = createOpenAIEncryptedToolCallReasoningTracker();
|
|
1469
|
+
const toolArgumentPreviewSchedules = /* @__PURE__ */ new WeakMap();
|
|
1470
|
+
const provisionalCommentaryTags = directMode ? options.provisionalCommentaryTags : /* @__PURE__ */ new Map();
|
|
1471
|
+
const blockIndices = /* @__PURE__ */ new WeakMap();
|
|
1472
|
+
let explicitVisibleTextBlocks;
|
|
1473
|
+
const normalizeToolCallDeltas = createOpenAICompletionsToolCallDeltaNormalizer();
|
|
1474
|
+
let finishReason;
|
|
1475
|
+
let sawNativeToolCallDelta = false;
|
|
1476
|
+
const blockIndex = () => directMode && currentBlock && currentBlock.type !== "toolCall" ? blockIndices.get(currentBlock) ?? output.content.length - 1 : output.content.length - 1;
|
|
1477
|
+
let chunkPushedEvent = false;
|
|
1478
|
+
const pushStreamEvent = (event) => {
|
|
1479
|
+
chunkPushedEvent = true;
|
|
1480
|
+
stream.push(event);
|
|
1481
|
+
};
|
|
1482
|
+
const appendToolCallBlock = (block) => {
|
|
1483
|
+
output.content.push(block);
|
|
1484
|
+
blockIndices.set(block, output.content.length - 1);
|
|
1485
|
+
pushStreamEvent({
|
|
1486
|
+
type: "toolcall_start",
|
|
1487
|
+
contentIndex: blockIndices.get(block) ?? -1,
|
|
1488
|
+
partial: output
|
|
1489
|
+
});
|
|
1490
|
+
};
|
|
1491
|
+
const queuePostToolCallDelta = (next) => {
|
|
1492
|
+
const nextBytes = Buffer.byteLength(next.text, "utf8");
|
|
1493
|
+
if (pendingPostToolCallBytes + nextBytes > MAX_POST_TOOL_CALL_BUFFER_BYTES) throw new Error("Exceeded post-tool-call delta buffer limit");
|
|
1494
|
+
pendingPostToolCallBytes += nextBytes;
|
|
1495
|
+
const previous = pendingPostToolCallDeltas[pendingPostToolCallDeltas.length - 1];
|
|
1496
|
+
if (!previous || previous.kind !== next.kind || previous.kind === "text" && next.kind === "text" && previous.source !== next.source || previous.kind === "thinking" && next.kind === "thinking" && previous.signature !== next.signature) {
|
|
1497
|
+
pendingPostToolCallDeltas.push(next);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
previous.text += next.text;
|
|
1501
|
+
};
|
|
1502
|
+
const appendContentDelta = (delta) => {
|
|
1503
|
+
flushPendingPostToolCallDeltas();
|
|
1504
|
+
if (directMode && directContent.block?.type === delta.kind) currentBlock = directContent.block;
|
|
1505
|
+
if (delta.kind === "text" && currentBlock?.type === "text" && currentTextSource !== delta.source) currentBlock = null;
|
|
1506
|
+
if (!currentBlock || currentBlock.type !== delta.kind) {
|
|
1507
|
+
options?.beforeContentBlock?.(delta.kind);
|
|
1508
|
+
if (delta.kind === "text") {
|
|
1509
|
+
currentBlock = {
|
|
1510
|
+
type: "text",
|
|
1511
|
+
text: ""
|
|
1512
|
+
};
|
|
1513
|
+
currentTextSource = delta.source;
|
|
1514
|
+
if (delta.source === "reasoning_detail") (explicitVisibleTextBlocks ??= /* @__PURE__ */ new Set()).add(currentBlock);
|
|
1515
|
+
} else currentBlock = {
|
|
1516
|
+
type: "thinking",
|
|
1517
|
+
thinking: "",
|
|
1518
|
+
...delta.signature ? { thinkingSignature: delta.signature } : {}
|
|
1519
|
+
};
|
|
1520
|
+
if (directMode) directContent.block = currentBlock;
|
|
1521
|
+
output.content.push(currentBlock);
|
|
1522
|
+
blockIndices.set(currentBlock, output.content.length - 1);
|
|
1523
|
+
pushStreamEvent({
|
|
1524
|
+
type: `${delta.kind}_start`,
|
|
1525
|
+
contentIndex: blockIndex(),
|
|
1526
|
+
partial: output
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
if (currentBlock.type === "thinking") appendAssistantThinking(currentBlock, delta.text);
|
|
1530
|
+
else {
|
|
1531
|
+
currentBlock.text += delta.text;
|
|
1532
|
+
if (pendingInterruptedTextBlock && delta.text.trim()) {
|
|
1533
|
+
confirmedInterruptedTextBlock = pendingInterruptedTextBlock;
|
|
1534
|
+
pendingInterruptedTextBlock = null;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const event = {
|
|
1538
|
+
contentIndex: blockIndex(),
|
|
1539
|
+
delta: delta.text
|
|
1540
|
+
};
|
|
1541
|
+
if (delta.kind === "thinking") pushStreamEvent({
|
|
1542
|
+
type: "thinking_delta",
|
|
1543
|
+
...event,
|
|
1544
|
+
partial: output
|
|
1545
|
+
});
|
|
1546
|
+
else pushStreamEvent({
|
|
1547
|
+
type: "text_delta",
|
|
1548
|
+
...event,
|
|
1549
|
+
...directMode ? { partial: output } : {}
|
|
1550
|
+
});
|
|
1551
|
+
};
|
|
1552
|
+
const flushPendingPostToolCallDeltas = () => {
|
|
1553
|
+
if (currentBlock?.type === "toolCall" || pendingPostToolCallDeltas.length === 0) return;
|
|
1554
|
+
const bufferedDeltas = pendingPostToolCallDeltas;
|
|
1555
|
+
pendingPostToolCallDeltas = [];
|
|
1556
|
+
pendingPostToolCallBytes = 0;
|
|
1557
|
+
for (const delta of bufferedDeltas) if (delta.kind === "text" || emitReasoning) appendContentDelta(delta);
|
|
1558
|
+
};
|
|
1559
|
+
const appendVisibleTextDelta = (text) => {
|
|
1560
|
+
if (!text) return;
|
|
1561
|
+
if (currentBlock?.type === "toolCall" && !directMode) queuePostToolCallDelta({
|
|
1562
|
+
kind: "text",
|
|
1563
|
+
text
|
|
1564
|
+
});
|
|
1565
|
+
else appendContentDelta({
|
|
1566
|
+
kind: "text",
|
|
1567
|
+
text
|
|
1568
|
+
});
|
|
1569
|
+
};
|
|
1570
|
+
const appendReasoningDeltas = (reasoningDeltas) => {
|
|
1571
|
+
for (const delta of reasoningDeltas) {
|
|
1572
|
+
if (delta.kind === "thinking" && !emitReasoning) continue;
|
|
1573
|
+
if (currentBlock?.type === "toolCall" && !directMode) {
|
|
1574
|
+
queuePostToolCallDelta({ ...delta });
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
appendContentDelta(delta.kind === "thinking" && directMode && model.provider === "opencode-go" && delta.signature === "reasoning" ? {
|
|
1578
|
+
...delta,
|
|
1579
|
+
signature: "reasoning_content"
|
|
1580
|
+
} : delta);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const appendRecoveredToolCall = (toolCall) => {
|
|
1584
|
+
if (currentBlock?.type === "toolCall") {
|
|
1585
|
+
currentBlock = null;
|
|
1586
|
+
flushPendingPostToolCallDeltas();
|
|
1587
|
+
}
|
|
1588
|
+
rememberPendingCommentaryTags(provisionalCommentaryTags, tagPendingCommentaryText(output.content));
|
|
1589
|
+
const block = {
|
|
1590
|
+
type: "toolCall",
|
|
1591
|
+
id: `call_${randomUUID().replaceAll("-", "").slice(0, 24)}`,
|
|
1592
|
+
name: toolCall.name,
|
|
1593
|
+
arguments: toolCall.arguments,
|
|
1594
|
+
partialArgs: toolCall.partialArgs
|
|
1595
|
+
};
|
|
1596
|
+
currentBlock = block;
|
|
1597
|
+
appendToolCallBlock(block);
|
|
1598
|
+
pushStreamEvent({
|
|
1599
|
+
type: "toolcall_delta",
|
|
1600
|
+
contentIndex: blockIndices.get(block) ?? -1,
|
|
1601
|
+
delta: toolCall.partialArgs,
|
|
1602
|
+
partial: output
|
|
1603
|
+
});
|
|
1604
|
+
};
|
|
1605
|
+
const appendRecoveredParts = (recoveredParts) => {
|
|
1606
|
+
for (const recoveredPart of recoveredParts) {
|
|
1607
|
+
if (recoveredPart.kind === "toolCall") {
|
|
1608
|
+
appendRecoveredToolCall(recoveredPart);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const parts = deepSeekTextFilter?.push(recoveredPart.text) ?? [recoveredPart.text];
|
|
1612
|
+
for (const part of parts) appendVisibleTextDelta(part);
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
const appendPartitionedVisibleDelta = (delta) => {
|
|
1616
|
+
if (delta.kind === "text") appendRecoveredParts(deepSeekToolCallRecoverer?.push(delta.text) ?? [{
|
|
1617
|
+
kind: "text",
|
|
1618
|
+
text: delta.text
|
|
1619
|
+
}]);
|
|
1620
|
+
};
|
|
1621
|
+
const emitReasoningUsageActivity = (hasReasoningUsageActivity) => {
|
|
1622
|
+
if (directMode || !hasReasoningUsageActivity || chunkPushedEvent || !emitReasoning) return;
|
|
1623
|
+
const latestBlock = output.content[output.content.length - 1];
|
|
1624
|
+
if (currentBlock?.type === "text" || currentBlock?.type === "toolCall") return;
|
|
1625
|
+
if (latestBlock?.type === "text" || latestBlock?.type === "toolCall") return;
|
|
1626
|
+
appendContentDelta({
|
|
1627
|
+
kind: "thinking",
|
|
1628
|
+
text: ""
|
|
1629
|
+
});
|
|
1630
|
+
};
|
|
1631
|
+
const flushReasoningTagTextPartitioner = () => {
|
|
1632
|
+
const recoverUnclosed = output.stopReason !== "length" && output.stopReason !== "error" && output.stopReason !== "aborted";
|
|
1633
|
+
for (const delta of reasoningTagTextPartitioner.flush({ recoverUnclosed })) appendPartitionedVisibleDelta(delta);
|
|
1634
|
+
};
|
|
1635
|
+
const flushGemmaToolCallRecoverer = (allowRecovery = true) => {
|
|
1636
|
+
for (const part of gemmaToolCallRecoverer?.flush(allowRecovery) ?? []) if (part.kind === "toolCall") appendRecoveredToolCall(part);
|
|
1637
|
+
else for (const delta of reasoningTagTextPartitioner.pushVisible(part.text)) appendPartitionedVisibleDelta(delta);
|
|
1638
|
+
};
|
|
1639
|
+
const sealTextBeforeReasoning = () => {
|
|
1640
|
+
if (currentBlock?.type !== "text" && !reasoningTagTextPartitioner.hasPending()) return;
|
|
1641
|
+
flushReasoningTagTextPartitioner();
|
|
1642
|
+
if (currentBlock?.type !== "text") return;
|
|
1643
|
+
if (currentTextSource !== "reasoning_detail" && currentBlock.text.trim()) pendingInterruptedTextBlock = currentBlock;
|
|
1644
|
+
currentBlock = null;
|
|
1645
|
+
if (directMode) directContent.block = null;
|
|
1646
|
+
currentTextSource = void 0;
|
|
1647
|
+
};
|
|
1648
|
+
const beginReasoning = (hasFollowingVisibleText, forceStrict = false) => {
|
|
1649
|
+
if (!output.openclawDelivery?.textPhaseRequiresTerminal) output.openclawDelivery = {
|
|
1650
|
+
...output.openclawDelivery,
|
|
1651
|
+
textPhaseRequiresTerminal: true
|
|
1652
|
+
};
|
|
1653
|
+
if (forceStrict || reasoningTagTextPartitioner.hasPending()) reasoningTagTextPartitioner.markStrict();
|
|
1654
|
+
if (!hasFollowingVisibleText || !reasoningTagTextPartitioner.hasPendingSyntax()) sealTextBeforeReasoning();
|
|
1655
|
+
};
|
|
1656
|
+
const guardedStream = withFirstStreamEventTimeout(responseStream, {
|
|
1657
|
+
provider: model.provider,
|
|
1658
|
+
api: model.api,
|
|
1659
|
+
model: model.id,
|
|
1660
|
+
timeoutMs: options?.firstEventTimeoutMs ?? 0,
|
|
1661
|
+
stage: "completions",
|
|
1662
|
+
abort: options?.abortFirstEventStream,
|
|
1663
|
+
onTimeout: options?.onFirstEventTimeout,
|
|
1664
|
+
hint: "The provider may be stalled while parsing the tool payload; retry with a smaller tool surface or enable OPENCLAW_DEBUG_MODEL_PAYLOAD=tools to inspect exposed tools."
|
|
1665
|
+
});
|
|
1666
|
+
const events = directMode ? guardedStream : iterateModelStream(guardedStream, options?.signal);
|
|
1667
|
+
for await (const rawChunk of events) {
|
|
1668
|
+
throwIfModelStreamAborted(options?.signal);
|
|
1669
|
+
chunkPushedEvent = false;
|
|
1670
|
+
if (!rawChunk || typeof rawChunk !== "object") continue;
|
|
1671
|
+
const chunk = rawChunk;
|
|
1672
|
+
output.responseId ||= chunk.id;
|
|
1673
|
+
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) output.responseModel ||= chunk.model;
|
|
1674
|
+
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : void 0;
|
|
1675
|
+
const usage = chunk.usage || choice?.usage;
|
|
1676
|
+
const hasReasoningUsageActivity = Boolean(asPositiveFiniteNumber(usage?.completion_tokens_details?.reasoning_tokens));
|
|
1677
|
+
if (usage) output.usage = parseOpenAICompletionsUsage(usage, model, { includeReasoningTokens: !directMode });
|
|
1678
|
+
const rawChoiceDelta = choice?.delta ?? choice?.message;
|
|
1679
|
+
notifyLlmRequestActivity(options?.signal, Boolean(usage || choice?.finish_reason || rawChoiceDelta && (rawChoiceDelta.tool_calls?.length || hasOpenAICompletionsDeltaContent(rawChoiceDelta))));
|
|
1680
|
+
if (!choice) {
|
|
1681
|
+
emitReasoningUsageActivity(hasReasoningUsageActivity);
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (choice.finish_reason) {
|
|
1685
|
+
const finishReasonResult = mapOpenAIStopReason(choice.finish_reason, { allowSingularToolCall: true });
|
|
1686
|
+
output.stopReason = finishReasonResult.stopReason;
|
|
1687
|
+
finishReason = finishReasonResult.stopReason;
|
|
1688
|
+
if (finishReasonResult.errorMessage) output.errorMessage = finishReasonResult.errorMessage;
|
|
1689
|
+
}
|
|
1690
|
+
if (!rawChoiceDelta) {
|
|
1691
|
+
emitReasoningUsageActivity(hasReasoningUsageActivity);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
for (const normalizedDelta of normalizeToolCallDeltas(rawChoiceDelta, choice.finish_reason)) {
|
|
1695
|
+
const choiceDelta = normalizedDelta.delta;
|
|
1696
|
+
const deltaFields = choiceDelta;
|
|
1697
|
+
const reasoningBatch = readOpenAICompletionsReasoningBatch(deltaFields, visibleReasoningDetailTypes);
|
|
1698
|
+
const reasoningDeltas = reasoningBatch.deltas;
|
|
1699
|
+
const hasReasoningThinking = reasoningBatch.hasThinking;
|
|
1700
|
+
const contentDeltas = readOpenAICompletionsContentDeltas(choiceDelta.content, choiceDelta.refusal, reasoningBatch.mirroredThinking);
|
|
1701
|
+
const lastVisibleTextIndex = contentDeltas.findLastIndex((delta) => delta.kind === "text");
|
|
1702
|
+
const hasSameChunkVisibleText = reasoningBatch.hasVisibleText || lastVisibleTextIndex !== -1;
|
|
1703
|
+
if (hasReasoningThinking) {
|
|
1704
|
+
beginReasoning(hasSameChunkVisibleText, true);
|
|
1705
|
+
appendReasoningDeltas(reasoningDeltas);
|
|
1706
|
+
}
|
|
1707
|
+
for (const [contentDeltaIndex, contentDelta] of contentDeltas.entries()) if (contentDelta.kind === "text") {
|
|
1708
|
+
const parts = gemmaToolCallRecoverer?.push(contentDelta.text) ?? [contentDelta];
|
|
1709
|
+
for (const part of parts) {
|
|
1710
|
+
const routedDeltas = hasReasoningThinking ? reasoningTagTextPartitioner.push(part.text) : reasoningTagTextPartitioner.pushVisible(part.text);
|
|
1711
|
+
for (const routedDelta of routedDeltas) appendPartitionedVisibleDelta(routedDelta);
|
|
1712
|
+
}
|
|
1713
|
+
} else {
|
|
1714
|
+
beginReasoning(contentDeltaIndex < lastVisibleTextIndex);
|
|
1715
|
+
if (emitReasoning) {
|
|
1716
|
+
if (currentBlock?.type === "toolCall" && !directMode) queuePostToolCallDelta(contentDelta);
|
|
1717
|
+
else appendContentDelta(contentDelta);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (!hasReasoningThinking) appendReasoningDeltas(reasoningDeltas);
|
|
1721
|
+
const toolCallDeltas = normalizedDelta.toolCalls;
|
|
1722
|
+
if (toolCallDeltas.length > 0) {
|
|
1723
|
+
flushGemmaToolCallRecoverer(false);
|
|
1724
|
+
sawNativeToolCallDelta = true;
|
|
1725
|
+
flushReasoningTagTextPartitioner();
|
|
1726
|
+
rememberPendingCommentaryTags(provisionalCommentaryTags, tagPendingCommentaryText(output.content));
|
|
1727
|
+
for (const toolCall of toolCallDeltas) {
|
|
1728
|
+
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : void 0;
|
|
1729
|
+
let block = streamIndex !== void 0 ? toolCallBlocksByIndex.get(streamIndex) : void 0;
|
|
1730
|
+
if (!block && toolCall.id) block = toolCallBlocksById.get(toolCall.id);
|
|
1731
|
+
if (!block) {
|
|
1732
|
+
if (currentBlock?.type === "toolCall") {
|
|
1733
|
+
currentBlock = null;
|
|
1734
|
+
flushPendingPostToolCallDeltas();
|
|
1735
|
+
}
|
|
1736
|
+
const initialSig = directMode ? void 0 : extractToolCallThoughtSignature(toolCall);
|
|
1737
|
+
options?.beforeContentBlock?.("toolCall");
|
|
1738
|
+
if (directMode && directContent.block?.type === "thinking") directContent.block = null;
|
|
1739
|
+
block = {
|
|
1740
|
+
type: "toolCall",
|
|
1741
|
+
id: toolCall.id || "",
|
|
1742
|
+
name: toolCall.function?.name || "",
|
|
1743
|
+
arguments: {},
|
|
1744
|
+
partialArgs: "",
|
|
1745
|
+
...initialSig ? { thoughtSignature: initialSig } : {}
|
|
1746
|
+
};
|
|
1747
|
+
encryptedReasoning.rememberToolCall(block.id, block);
|
|
1748
|
+
toolArgumentPreviewSchedules.set(block, createToolArgumentPreviewSchedule());
|
|
1749
|
+
appendToolCallBlock(block);
|
|
1750
|
+
}
|
|
1751
|
+
if (streamIndex !== void 0 && !toolCallBlocksByIndex.has(streamIndex)) toolCallBlocksByIndex.set(streamIndex, block);
|
|
1752
|
+
if (toolCall.id) {
|
|
1753
|
+
const previousId = block.id;
|
|
1754
|
+
if (!directMode || !block.id) block.id = toolCall.id;
|
|
1755
|
+
toolCallBlocksById.set(toolCall.id, block);
|
|
1756
|
+
if (block.id === toolCall.id) encryptedReasoning.rememberToolCall(toolCall.id, block, previousId);
|
|
1757
|
+
}
|
|
1758
|
+
currentBlock = block;
|
|
1759
|
+
const conflictingId = directMode && block.id && toolCall.id && block.id !== toolCall.id;
|
|
1760
|
+
if (toolCall.function?.name && !conflictingId) block.name = toolCall.function.name;
|
|
1761
|
+
const deltaSig = directMode ? void 0 : extractToolCallThoughtSignature(toolCall);
|
|
1762
|
+
if (deltaSig) block.thoughtSignature = deltaSig;
|
|
1763
|
+
const toolArgumentsDelta = toolCall.function?.arguments;
|
|
1764
|
+
if (toolArgumentsDelta) {
|
|
1765
|
+
block.partialArgs += toolArgumentsDelta;
|
|
1766
|
+
if (toolArgumentPreviewSchedules.get(block)?.(block.partialArgs.length)) block.arguments = parseStreamingJson(block.partialArgs);
|
|
1767
|
+
}
|
|
1768
|
+
if (toolArgumentsDelta || directMode) pushStreamEvent({
|
|
1769
|
+
type: "toolcall_delta",
|
|
1770
|
+
contentIndex: blockIndices.get(block) ?? -1,
|
|
1771
|
+
delta: toolArgumentsDelta ?? "",
|
|
1772
|
+
partial: output
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
encryptedReasoning.consumeDetails(deltaFields.reasoning_details);
|
|
1777
|
+
}
|
|
1778
|
+
flushPendingPostToolCallDeltas();
|
|
1779
|
+
emitReasoningUsageActivity(hasReasoningUsageActivity);
|
|
1780
|
+
}
|
|
1781
|
+
throwIfModelStreamAborted(options?.signal);
|
|
1782
|
+
if (!finishReason && (directMode || options?.sawStreamDONE?.() === false)) throw new Error("Stream ended without finish_reason");
|
|
1783
|
+
flushGemmaToolCallRecoverer();
|
|
1784
|
+
flushReasoningTagTextPartitioner();
|
|
1785
|
+
appendRecoveredParts(deepSeekToolCallRecoverer?.flush() ?? []);
|
|
1786
|
+
for (const part of deepSeekTextFilter?.flush() ?? []) appendVisibleTextDelta(part);
|
|
1787
|
+
currentBlock = null;
|
|
1788
|
+
flushPendingPostToolCallDeltas();
|
|
1789
|
+
finalizeOpenAICompletionsToolCalls(output, {
|
|
1790
|
+
allowSilentToolCallPromotion: finishReason === "stop" || sawNativeToolCallDelta && (options?.sawStreamDONE?.() ?? false),
|
|
1791
|
+
onConfirmedToolCall(block, contentIndex) {
|
|
1792
|
+
if (directMode) return;
|
|
1793
|
+
pushStreamEvent({
|
|
1794
|
+
type: "toolcall_end",
|
|
1795
|
+
contentIndex,
|
|
1796
|
+
toolCall: block,
|
|
1797
|
+
partial: output
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
if (confirmedInterruptedTextBlock && output.stopReason !== "toolUse" && output.stopReason !== "error" && output.stopReason !== "aborted") tagInterruptedTextPhases(output.content, confirmedInterruptedTextBlock, explicitVisibleTextBlocks);
|
|
1802
|
+
if (output.stopReason !== "toolUse") clearPendingCommentaryText(provisionalCommentaryTags);
|
|
1803
|
+
if (output.stopReason === "error" || output.stopReason === "aborted") tagUnresolvedTextAsCommentary(output);
|
|
1804
|
+
if (output.stopReason === "toolUse") tagPendingCommentaryText(output.content);
|
|
1805
|
+
if (!output.usage.contextUsage && !options?.signal?.aborted && output.stopReason !== "error" && output.stopReason !== "aborted") {
|
|
1806
|
+
output.usage.contextUsage = { state: "unavailable" };
|
|
1807
|
+
if (!compat.supportsUsageInStreaming) warnMissingStreamingUsage(openAIModel);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const MAX_MISSING_USAGE_HINT_KEYS = 256;
|
|
1811
|
+
const missingUsageHintKeys = /* @__PURE__ */ new Set();
|
|
1812
|
+
function warnMissingStreamingUsage(model) {
|
|
1813
|
+
if (detectOpenAICompletionsCompat(model).capabilities.endpointClass !== "custom") return;
|
|
1814
|
+
const key = `${model.provider}/${model.id}`;
|
|
1815
|
+
if (missingUsageHintKeys.has(key) || missingUsageHintKeys.size >= MAX_MISSING_USAGE_HINT_KEYS) return;
|
|
1816
|
+
missingUsageHintKeys.add(key);
|
|
1817
|
+
log.warn(`${key} returned no token usage; context size is estimated and token accounting is unavailable. If this endpoint supports stream_options.include_usage, set compat.supportsUsageInStreaming: true on the model in models.providers.${model.provider}.models.`, {
|
|
1818
|
+
provider: model.provider,
|
|
1819
|
+
model: model.id
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function shouldEmitOpenAICompletionsReasoning(model, options) {
|
|
1823
|
+
if (!model.reasoning) return false;
|
|
1824
|
+
const effort = options?.reasoningEffort ?? options?.reasoning ?? "high";
|
|
1825
|
+
return Boolean(effort) && isOpenAICompletionsThinkingEnabled(effort);
|
|
1826
|
+
}
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region packages/ai/src/providers/openai-web-search-tools.ts
|
|
1829
|
+
function hasResponsesWebSearchTool(tools) {
|
|
1830
|
+
if (!Array.isArray(tools)) return false;
|
|
1831
|
+
return tools.some((tool) => {
|
|
1832
|
+
if (!isRecord(tool)) return false;
|
|
1833
|
+
if (tool.type === "web_search") return true;
|
|
1834
|
+
if (tool.type === "function" && tool.name === "web_search") return true;
|
|
1835
|
+
const fn = tool.function;
|
|
1836
|
+
return isRecord(fn) && fn.name === "web_search";
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region packages/ai/src/transports/openai-responses-params-internal.ts
|
|
1841
|
+
const OPENAI_RESPONSES_TOOL_CALL_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1842
|
+
"openai",
|
|
1843
|
+
"opencode",
|
|
1844
|
+
"azure-openai-responses",
|
|
1845
|
+
"github-copilot"
|
|
1846
|
+
]);
|
|
1847
|
+
function raiseMinimalReasoningForResponsesWebSearch(params) {
|
|
1848
|
+
if (params.effort !== "minimal" || !hasResponsesWebSearchTool(params.tools)) return params.effort;
|
|
1849
|
+
for (const effort of [
|
|
1850
|
+
"low",
|
|
1851
|
+
"medium",
|
|
1852
|
+
"high"
|
|
1853
|
+
]) {
|
|
1854
|
+
const resolved = resolveOpenAIRequestReasoning(params.model, effort).effort;
|
|
1855
|
+
if (resolved && resolved !== "none" && resolved !== "minimal") return resolved;
|
|
1856
|
+
}
|
|
1857
|
+
return params.effort;
|
|
1858
|
+
}
|
|
1859
|
+
const OPENAI_CODEX_RESPONSES_UNSUPPORTED_PARAMS = [
|
|
1860
|
+
"max_output_tokens",
|
|
1861
|
+
"metadata",
|
|
1862
|
+
"prompt_cache_retention",
|
|
1863
|
+
"prompt_cache_options",
|
|
1864
|
+
"service_tier",
|
|
1865
|
+
"temperature",
|
|
1866
|
+
"top_p"
|
|
1867
|
+
];
|
|
1868
|
+
function stripOpenAICodexResponsesUnsupportedTextFields(params) {
|
|
1869
|
+
const text = params.text;
|
|
1870
|
+
if (!text || typeof text !== "object" || Array.isArray(text)) return;
|
|
1871
|
+
const sanitizedText = { ...text };
|
|
1872
|
+
delete sanitizedText.format;
|
|
1873
|
+
if (Object.keys(sanitizedText).length > 0) params.text = sanitizedText;
|
|
1874
|
+
else delete params.text;
|
|
1875
|
+
}
|
|
1876
|
+
function sanitizeOpenAICodexResponsesParams(model, params) {
|
|
1877
|
+
if (!usesNativeOpenAICodexResponsesBackend(model)) return params;
|
|
1878
|
+
for (const key of OPENAI_CODEX_RESPONSES_UNSUPPORTED_PARAMS) delete params[key];
|
|
1879
|
+
Object.assign(params, { store: false });
|
|
1880
|
+
stripOpenAICodexResponsesUnsupportedTextFields(params);
|
|
1881
|
+
return params;
|
|
1882
|
+
}
|
|
1883
|
+
function buildOpenAIResponsesInstructionsText(context) {
|
|
1884
|
+
if (!context.systemPrompt) return;
|
|
1885
|
+
return sanitizeTransportPayloadText(stripSystemPromptCacheBoundary(context.systemPrompt));
|
|
1886
|
+
}
|
|
1887
|
+
function resolveOpenAIResponsesInstructions(model, context, usesInstructionsField) {
|
|
1888
|
+
if (!usesInstructionsField) return;
|
|
1889
|
+
const instructions = buildOpenAIResponsesInstructionsText(context);
|
|
1890
|
+
if (instructions && instructions.trim().length > 0) return instructions;
|
|
1891
|
+
return usesNativeOpenAICodexResponsesBackend(model) ? OPENAI_CODEX_RESPONSES_DEFAULT_INSTRUCTIONS : void 0;
|
|
1892
|
+
}
|
|
1893
|
+
function buildOpenAIResponsesCompactSystemMessage(model, instructions) {
|
|
1894
|
+
const compat = getCompat(model);
|
|
1895
|
+
const role = model.reasoning && compat.supportsDeveloperRole ? "developer" : "system";
|
|
1896
|
+
return buildResponsesInputMessage(role, [{
|
|
1897
|
+
type: "input_text",
|
|
1898
|
+
text: instructions
|
|
1899
|
+
}]);
|
|
1900
|
+
}
|
|
1901
|
+
function ensureOpenAIResponsesNonEmptyInput(messages, context) {
|
|
1902
|
+
if (messages.length > 0 || !context.systemPrompt) return;
|
|
1903
|
+
if (!buildOpenAIResponsesInstructionsText(context)) throw new Error("OpenAI Responses requires non-empty input when only systemPrompt is provided.");
|
|
1904
|
+
messages.push(buildResponsesInputMessage("user", [{
|
|
1905
|
+
type: "input_text",
|
|
1906
|
+
text: " "
|
|
1907
|
+
}]));
|
|
1908
|
+
}
|
|
1909
|
+
function buildOpenAIResponsesParams(model, context, options, metadata, replayMode = "checkpoint") {
|
|
1910
|
+
const payloadPolicy = resolveOpenAIResponsesPayloadPolicy(model, { storeMode: "transport-default" });
|
|
1911
|
+
const policyAllowsReplayIds = payloadPolicy.explicitStore !== false && !payloadPolicy.shouldStripStore;
|
|
1912
|
+
const replayResponsesItemIds = !usesNativeOpenAICodexResponsesBackend(model) && (options?.replayResponsesItemIds ?? policyAllowsReplayIds);
|
|
1913
|
+
const messages = convertResponsesMessages(model, context, OPENAI_RESPONSES_TOOL_CALL_PROVIDERS, {
|
|
1914
|
+
includeSystemPrompt: !payloadPolicy.usesInstructionsField,
|
|
1915
|
+
replayReasoningItems: true,
|
|
1916
|
+
replayResponsesItemIds,
|
|
1917
|
+
authProfileId: options?.authProfileId,
|
|
1918
|
+
sessionId: options?.sessionId,
|
|
1919
|
+
replayMode
|
|
1920
|
+
});
|
|
1921
|
+
ensureOpenAIResponsesNonEmptyInput(messages, context);
|
|
1922
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
1923
|
+
const compat = getCompat(model);
|
|
1924
|
+
const promptCacheKey = compat.supportsPromptCacheKey ? resolvePromptCacheKey(options, cacheRetention) : void 0;
|
|
1925
|
+
const instructions = resolveOpenAIResponsesInstructions(model, context, payloadPolicy.usesInstructionsField);
|
|
1926
|
+
const params = {
|
|
1927
|
+
model: model.id,
|
|
1928
|
+
input: messages,
|
|
1929
|
+
stream: true,
|
|
1930
|
+
prompt_cache_key: promptCacheKey,
|
|
1931
|
+
...resolveOpenAIPromptCacheParams(model, cacheRetention, compat),
|
|
1932
|
+
...instructions ? { instructions } : {},
|
|
1933
|
+
...metadata ? { metadata } : {}
|
|
1934
|
+
};
|
|
1935
|
+
const effectiveMaxTokens = options?.maxTokens || model.maxTokens;
|
|
1936
|
+
if (effectiveMaxTokens) params.max_output_tokens = Math.max(effectiveMaxTokens, 16);
|
|
1937
|
+
if (options?.temperature !== void 0 && supportsOpenAITemperature(model)) params.temperature = options.temperature;
|
|
1938
|
+
if (options?.topP !== void 0 && (!isOpenAIGpt6Model(model) || resolveOpenAIThinkingApi(model.api) === "azure-openai-responses")) params.top_p = options.topP;
|
|
1939
|
+
if (options?.responseFormat !== void 0) params.text = {
|
|
1940
|
+
...params.text,
|
|
1941
|
+
format: resolveOpenAIResponsesTextFormat(options.responseFormat)
|
|
1942
|
+
};
|
|
1943
|
+
if (options?.serviceTier !== void 0 && payloadPolicy.allowsServiceTier) params.service_tier = options.serviceTier;
|
|
1944
|
+
if (context.tools) {
|
|
1945
|
+
const tools = context.tools;
|
|
1946
|
+
const strict = getAiTransportHost().resolveOpenAIStrictToolSetting(model, {
|
|
1947
|
+
transport: "stream",
|
|
1948
|
+
supportsStrictMode: compat.supportsStrictMode
|
|
1949
|
+
});
|
|
1950
|
+
const { projection, tools: converted } = prepareResponsesTools(tools, strict, model);
|
|
1951
|
+
if (converted.length > 0 || projection.inputToolCount === 0 && projection.diagnostics.length === 0) params.tools = converted;
|
|
1952
|
+
if (options?.toolChoice) {
|
|
1953
|
+
const toolChoice = reconcileOpenAIResponsesToolChoice(options.toolChoice, projection);
|
|
1954
|
+
if (toolChoice !== void 0) params.tool_choice = toolChoice;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (model.reasoning) {
|
|
1958
|
+
const reasoning = options?.reasoning;
|
|
1959
|
+
const requestedEffort = options?.reasoningEffort ?? (reasoning === "none" ? "none" : resolveOpenAISimpleReasoningEffort(model, reasoning)) ?? (options?.reasoningSummary ? "high" : payloadPolicy.defaultManagedReasoningEffort);
|
|
1960
|
+
const resolvedEffort = requestedEffort === void 0 ? void 0 : resolveOpenAIRequestReasoning(model, requestedEffort).effort;
|
|
1961
|
+
if (resolvedEffort !== void 0) {
|
|
1962
|
+
const effort = raiseMinimalReasoningForResponsesWebSearch({
|
|
1963
|
+
model,
|
|
1964
|
+
effort: resolvedEffort,
|
|
1965
|
+
tools: params.tools
|
|
1966
|
+
});
|
|
1967
|
+
const summary = effort !== "none" && (options?.reasoningEffort || options?.reasoning || options?.reasoningSummary) ? options.reasoningSummary || "auto" : void 0;
|
|
1968
|
+
params.reasoning = {
|
|
1969
|
+
effort,
|
|
1970
|
+
...summary ? { summary } : {}
|
|
1971
|
+
};
|
|
1972
|
+
if (summary) params.include = ["reasoning.encrypted_content"];
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
applyOpenAIResponsesPayloadPolicy(params, payloadPolicy);
|
|
1976
|
+
return sanitizeOpenAICodexResponsesParams(model, params);
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region packages/ai/src/transports/openai-responses-compact-client.ts
|
|
1980
|
+
async function postOpenAIResponsesCompaction(params) {
|
|
1981
|
+
const compactInput = typeof params.request.instructions === "string" && params.request.instructions.length > 0 ? [buildOpenAIResponsesCompactSystemMessage(params.model, params.request.instructions), ...params.request.input ?? []] : params.request.input;
|
|
1982
|
+
const response = await params.client.post("/responses/compact", {
|
|
1983
|
+
...buildOpenAISdkRequestOptions(params.model, params.options?.signal, { timeoutMs: params.options?.timeoutMs }),
|
|
1984
|
+
body: {
|
|
1985
|
+
model: params.request.model,
|
|
1986
|
+
input: compactInput
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
const output = isRecord(response) && Array.isArray(response.output) ? response.output : [];
|
|
1990
|
+
const item = output.at(-1);
|
|
1991
|
+
const retainedItems = output.slice(0, -1);
|
|
1992
|
+
const retainedUserMessageCount = retainedItems.filter((candidate) => isRecord(candidate) && candidate.type === "message" && candidate.role === "user" && Array.isArray(candidate.content)).length;
|
|
1993
|
+
const inputUserMessageCount = Array.isArray(params.request.input) ? params.request.input.filter((candidate) => isRecord(candidate) && candidate.type === "message" && candidate.role === "user").length : 0;
|
|
1994
|
+
const retainedMessagePrefixSupported = supportsNativeOpenAIResponsesEndpoint(params.model);
|
|
1995
|
+
const usage = isRecord(response) && isRecord(response.usage) ? response.usage : void 0;
|
|
1996
|
+
if (!isRecord(response) || response.object !== "response.compaction" || !isOpenAIResponsesCompactionOutput(output, params.model) || retainedItems.length > 0 && (!retainedMessagePrefixSupported || retainedUserMessageCount !== inputUserMessageCount) || !isRecord(item) || item.type !== "compaction" || typeof item.encrypted_content !== "string" || item.encrypted_content.length === 0 || !usage || typeof usage.input_tokens !== "number" || typeof usage.output_tokens !== "number") throw new Error("Responses compact endpoint did not return one trailing compaction item");
|
|
1997
|
+
return {
|
|
1998
|
+
output,
|
|
1999
|
+
item,
|
|
2000
|
+
historyMode: retainedUserMessageCount > 0 ? "retained-users" : "compacted-prefix",
|
|
2001
|
+
usage,
|
|
2002
|
+
model: params.model,
|
|
2003
|
+
replayMetadata: buildOpenAIResponsesReasoningReplayMetadata(params.model, {
|
|
2004
|
+
authProfileId: params.options?.authProfileId,
|
|
2005
|
+
sessionId: params.options?.sessionId
|
|
2006
|
+
})
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
//#endregion
|
|
2010
|
+
//#region packages/ai/src/transports/openai-responses-compact-request.ts
|
|
2011
|
+
const COMPACT_REQUEST = Symbol("openaiResponsesCompactRequest");
|
|
2012
|
+
function claimResponsesCompactRequest(options) {
|
|
2013
|
+
const controller = options ? Reflect.get(options, COMPACT_REQUEST) : void 0;
|
|
2014
|
+
if (controller?.claimed === false) {
|
|
2015
|
+
controller.claimed = true;
|
|
2016
|
+
return controller;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/** Run a compact-endpoint request through the session's prepared stream stack. */
|
|
2020
|
+
async function requestPreparedOpenAIResponsesCompaction(streamFn, model, context, options) {
|
|
2021
|
+
const preparedOptions = { ...options };
|
|
2022
|
+
let resolveResult;
|
|
2023
|
+
let rejectResult;
|
|
2024
|
+
const result = new Promise((resolve, reject) => {
|
|
2025
|
+
resolveResult = resolve;
|
|
2026
|
+
rejectResult = reject;
|
|
2027
|
+
});
|
|
2028
|
+
const controller = {
|
|
2029
|
+
claimed: false,
|
|
2030
|
+
resolve: resolveResult,
|
|
2031
|
+
reject: rejectResult
|
|
2032
|
+
};
|
|
2033
|
+
Reflect.set(preparedOptions, COMPACT_REQUEST, controller);
|
|
2034
|
+
const stream = await Promise.resolve(streamFn(model, context, preparedOptions));
|
|
2035
|
+
if (!controller.claimed) throw new Error("Prepared stream did not reach an OpenAI Responses transport");
|
|
2036
|
+
try {
|
|
2037
|
+
return await result;
|
|
2038
|
+
} finally {
|
|
2039
|
+
await stream.result().catch(() => void 0);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region packages/ai/src/transports/openai-responses-reasoning-state.ts
|
|
2044
|
+
/** Save only admitted settings and hashes, never another copy of the conversation. */
|
|
2045
|
+
function recordResponsesReasoningState(message, model, identity, request, output) {
|
|
2046
|
+
if (!supportsResponsesReasoningUpdate(request) || !isRecord(request.reasoning) || !request.input || request.previous_response_id || message.providerReplay || output.some((item) => isRecord(item) && item.type === "compaction")) return;
|
|
2047
|
+
const reasoning = {
|
|
2048
|
+
...buildProviderReplayContext(model, identity),
|
|
2049
|
+
effort: request.reasoning.effort,
|
|
2050
|
+
controls: request.input.flatMap((item, index) => isConfigurationUpdate(item) ? [{
|
|
2051
|
+
index,
|
|
2052
|
+
item
|
|
2053
|
+
}] : []),
|
|
2054
|
+
inputLength: request.input.length,
|
|
2055
|
+
outputLength: output.length,
|
|
2056
|
+
prefixHash: responsesContinuationPrefixFingerprint(request.input, output),
|
|
2057
|
+
requestHash: responsesContinuationRequestFingerprint(request)
|
|
2058
|
+
};
|
|
2059
|
+
Object.assign(message, { openclawResponsesInputReplay: {
|
|
2060
|
+
...readResponsesInputReplayState(message),
|
|
2061
|
+
reasoning
|
|
2062
|
+
} });
|
|
2063
|
+
}
|
|
2064
|
+
/** A cold transport can replay controls, but cannot resurrect a server response handle. */
|
|
2065
|
+
function restoreResponsesReasoningState(context, model, identity, request) {
|
|
2066
|
+
const latest = context.messages.findLast((message) => message.role === "assistant");
|
|
2067
|
+
const state = latest ? readResponsesInputReplayState(latest)?.reasoning : void 0;
|
|
2068
|
+
if (!isRecord(state)) return request;
|
|
2069
|
+
const { effort, inputLength, outputLength, controls, prefixHash, requestHash } = state;
|
|
2070
|
+
if (!isProviderReplayContext(state) || !providerReplayContextMatches(state, buildProviderReplayContext(model, identity)) || latest?.providerReplay || !supportsResponsesReasoningUpdate(request) || !isRecord(request.reasoning) || !request.input || request.previous_response_id || request.input.some(isConfigurationUpdate) || typeof effort !== "string" || typeof inputLength !== "number" || !Number.isSafeInteger(inputLength) || inputLength < 0 || typeof outputLength !== "number" || !Number.isSafeInteger(outputLength) || outputLength < 0 || !Array.isArray(controls) || controls.length > inputLength || inputLength + outputLength > request.input.length + controls.length) return request;
|
|
2071
|
+
const previousInput = request.input.slice(0, inputLength - controls.length);
|
|
2072
|
+
let lastIndex = -1;
|
|
2073
|
+
for (const control of controls) {
|
|
2074
|
+
if (!isRecord(control) || typeof control.index !== "number" || !Number.isSafeInteger(control.index) || control.index <= lastIndex || control.index > previousInput.length || !isConfigurationUpdate(control.item)) return request;
|
|
2075
|
+
previousInput.splice(control.index, 0, control.item);
|
|
2076
|
+
lastIndex = control.index;
|
|
2077
|
+
}
|
|
2078
|
+
const previous = {
|
|
2079
|
+
...request,
|
|
2080
|
+
reasoning: {
|
|
2081
|
+
...request.reasoning,
|
|
2082
|
+
effort
|
|
2083
|
+
},
|
|
2084
|
+
input: previousInput
|
|
2085
|
+
};
|
|
2086
|
+
if (responsesContinuationRequestFingerprint(previous) !== requestHash) return request;
|
|
2087
|
+
const prepared = replayResponsesReasoningUpdates(previous, request, outputLength);
|
|
2088
|
+
if (responsesContinuationPrefixFingerprint((prepared.input ?? []).slice(0, inputLength + outputLength)) !== prefixHash) return request;
|
|
2089
|
+
return prepared;
|
|
2090
|
+
}
|
|
2091
|
+
//#endregion
|
|
2092
|
+
//#region packages/ai/src/transports/openai-responses-request-fetch.ts
|
|
2093
|
+
function withDefaultResponsesStreamEncoding(fetch) {
|
|
2094
|
+
return (input, init) => {
|
|
2095
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
|
|
2096
|
+
if (headers.has("accept-encoding")) return fetch(input, init);
|
|
2097
|
+
headers.set("accept-encoding", "identity");
|
|
2098
|
+
return fetch(input, {
|
|
2099
|
+
...init,
|
|
2100
|
+
headers
|
|
2101
|
+
});
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/** Preserve the existing fetch policy while binding explicit continuation dispatch to its signal. */
|
|
2105
|
+
function createResponsesRequestFetch(model, options) {
|
|
2106
|
+
const lifecycle = options.lifecycle;
|
|
2107
|
+
if (lifecycle && options.compact) throw new Error("Provider review continuation cannot replace its reviewed input with compaction");
|
|
2108
|
+
let fetchOverride = options.compact ? createBoundedOpenAIResponsesCompactionFetch(buildGuardedModelFetch(model)) : options.stream ? withDefaultResponsesStreamEncoding(buildGuardedModelFetch(model, void 0, { onSseComment: options.onSseComment })) : void 0;
|
|
2109
|
+
if (lifecycle) {
|
|
2110
|
+
const dispatchFetch = fetchOverride ?? buildGuardedModelFetch(model);
|
|
2111
|
+
fetchOverride = async (input, init) => {
|
|
2112
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
2113
|
+
await lifecycle.beforeDispatch(signal ?? void 0);
|
|
2114
|
+
signal?.throwIfAborted();
|
|
2115
|
+
lifecycle.assertCurrent();
|
|
2116
|
+
return dispatchFetch(input, init);
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
return fetchOverride;
|
|
2120
|
+
}
|
|
2121
|
+
//#endregion
|
|
2122
|
+
//#region packages/ai/src/transports/openai-responses-steering.ts
|
|
2123
|
+
function cloneWireRequest(request) {
|
|
2124
|
+
const serialized = JSON.stringify(request);
|
|
2125
|
+
return JSON.parse(serialized);
|
|
2126
|
+
}
|
|
2127
|
+
async function projectResponsesSteeringInput(request, project) {
|
|
2128
|
+
const { input: activeInput, ...activeSettings } = cloneWireRequest(request);
|
|
2129
|
+
if (!Array.isArray(activeInput)) return [];
|
|
2130
|
+
const { input, ...settings } = cloneWireRequest(await project());
|
|
2131
|
+
if (!Array.isArray(input) || stableStringify(settings) !== stableStringify(activeSettings) || stableStringify(input.slice(0, activeInput.length)) !== stableStringify(activeInput)) return [];
|
|
2132
|
+
const projected = input.slice(activeInput.length);
|
|
2133
|
+
return projected.every((item) => isRecord(item) && item.role === "user") ? projected : [];
|
|
2134
|
+
}
|
|
2135
|
+
/** One response owns admission; accepted input stays on this connection until continuation. */
|
|
2136
|
+
function createResponsesSteering(params) {
|
|
2137
|
+
let responseId;
|
|
2138
|
+
let sealed = false;
|
|
2139
|
+
let unsubscribe;
|
|
2140
|
+
const pending = [];
|
|
2141
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
2142
|
+
const acknowledged = /* @__PURE__ */ new Set();
|
|
2143
|
+
const seal = () => {
|
|
2144
|
+
sealed = true;
|
|
2145
|
+
const cleanup = unsubscribe;
|
|
2146
|
+
unsubscribe = void 0;
|
|
2147
|
+
cleanup?.();
|
|
2148
|
+
};
|
|
2149
|
+
return {
|
|
2150
|
+
get responseId() {
|
|
2151
|
+
return responseId;
|
|
2152
|
+
},
|
|
2153
|
+
get pending() {
|
|
2154
|
+
return pending.length > 0;
|
|
2155
|
+
},
|
|
2156
|
+
get acceptedInput() {
|
|
2157
|
+
return [...accepted.values()].flat();
|
|
2158
|
+
},
|
|
2159
|
+
seal,
|
|
2160
|
+
close(error) {
|
|
2161
|
+
seal();
|
|
2162
|
+
for (const submission of pending.splice(0)) submission.reject(error);
|
|
2163
|
+
},
|
|
2164
|
+
handle(event) {
|
|
2165
|
+
if (!isRecord(event)) return false;
|
|
2166
|
+
if (event.type === "response.created" && !responseId && !sealed) {
|
|
2167
|
+
const response = isRecord(event.response) ? event.response : void 0;
|
|
2168
|
+
if (typeof response?.id !== "string" || !response.id.trim()) throw new Error("Responses steering requires a response identity");
|
|
2169
|
+
const activeResponseId = response.id;
|
|
2170
|
+
responseId = activeResponseId;
|
|
2171
|
+
const cleanup = params.onActiveResponse({
|
|
2172
|
+
needsContinuation: params.needsContinuation,
|
|
2173
|
+
steer(messages) {
|
|
2174
|
+
if (sealed || messages.length === 0) return Promise.resolve(false);
|
|
2175
|
+
params.assertActive();
|
|
2176
|
+
const converted = params.toInput(messages);
|
|
2177
|
+
const submit = (input) => {
|
|
2178
|
+
if (sealed || input.length === 0) return Promise.resolve(false);
|
|
2179
|
+
params.assertActive();
|
|
2180
|
+
return new Promise((resolve, reject) => {
|
|
2181
|
+
pending.push({
|
|
2182
|
+
input,
|
|
2183
|
+
resolve,
|
|
2184
|
+
reject
|
|
2185
|
+
});
|
|
2186
|
+
params.send({
|
|
2187
|
+
type: "response.steer",
|
|
2188
|
+
previous_response_id: activeResponseId,
|
|
2189
|
+
input
|
|
2190
|
+
});
|
|
2191
|
+
});
|
|
2192
|
+
};
|
|
2193
|
+
return Array.isArray(converted) ? submit(converted) : converted.then(submit);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
if (sealed) cleanup?.();
|
|
2197
|
+
else unsubscribe = cleanup;
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
if (event.type !== "response.steer.accepted" && event.type !== "response.steer.failed" && event.type !== "response.steer.pending") return false;
|
|
2201
|
+
const steer = isRecord(event.steer) ? event.steer : void 0;
|
|
2202
|
+
if (!responseId || steer?.previous_response_id !== responseId) throw new Error("Responses steering acknowledgement has an unexpected identity");
|
|
2203
|
+
if (event.type === "response.steer.failed" && steer.id === void 0) {
|
|
2204
|
+
const index = pending.findIndex((submission) => stableStringify(submission.input) === stableStringify(steer.input));
|
|
2205
|
+
const submission = pending[index];
|
|
2206
|
+
if (!submission) throw new Error("Responses steering acknowledgement has no pending submission");
|
|
2207
|
+
pending.splice(index, 1);
|
|
2208
|
+
submission.resolve(false);
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
if (typeof steer.id !== "string" || !steer.id.trim()) throw new Error("Responses steering acknowledgement has an unexpected identity");
|
|
2212
|
+
if (event.type === "response.steer.pending") {
|
|
2213
|
+
if (!accepted.has(steer.id)) throw new Error("Responses steering pending event has no accepted submission");
|
|
2214
|
+
return true;
|
|
2215
|
+
}
|
|
2216
|
+
if (event.type === "response.steer.failed" && accepted.has(steer.id)) throw new Error("OpenAI could not apply accepted steering; the queued message remains in the conversation");
|
|
2217
|
+
if (acknowledged.has(steer.id)) throw new Error("Responses steering acknowledgement was already applied");
|
|
2218
|
+
const submission = pending.shift();
|
|
2219
|
+
if (!submission) throw new Error("Responses steering acknowledgement has no pending submission");
|
|
2220
|
+
acknowledged.add(steer.id);
|
|
2221
|
+
if (event.type === "response.steer.accepted") {
|
|
2222
|
+
accepted.set(steer.id, submission.input);
|
|
2223
|
+
submission.resolve(true);
|
|
2224
|
+
} else submission.resolve(false);
|
|
2225
|
+
return true;
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
/** Accepted steering is prepended by the server, so never repeat it in response.create. */
|
|
2230
|
+
function omitAcceptedSteering(input, accepted) {
|
|
2231
|
+
const remaining = [...input];
|
|
2232
|
+
for (const message of accepted) {
|
|
2233
|
+
const index = remaining.findIndex((item) => stableStringify(item) === stableStringify(message));
|
|
2234
|
+
if (index < 0) throw new Error("Responses steering continuation no longer contains the accepted user input");
|
|
2235
|
+
remaining.splice(index, 1);
|
|
2236
|
+
}
|
|
2237
|
+
return remaining;
|
|
2238
|
+
}
|
|
2239
|
+
//#endregion
|
|
2240
|
+
//#region packages/ai/src/transports/openai-responses-websocket-recovery.ts
|
|
2241
|
+
/** Owns safe WebSocket rejection recovery; transport selection and session leases stay with the caller. */
|
|
2242
|
+
function createRecoverableResponsesWebSocketStream(params) {
|
|
2243
|
+
const { trackedWebSocketStream, websocket, websocketSignal, options, output, buildRequest, createSseStream, closeWebSocketForFallback, logWebSocketFallback, startStream } = params;
|
|
2244
|
+
return { async *[Symbol.asyncIterator]() {
|
|
2245
|
+
let acceptance = "pending";
|
|
2246
|
+
let outputObserved = false;
|
|
2247
|
+
try {
|
|
2248
|
+
for await (const event of trackedWebSocketStream) {
|
|
2249
|
+
const { failure, hasResponseOutput, isPrelude } = readResponsesRecoveryEvent(event);
|
|
2250
|
+
outputObserved ||= hasResponseOutput;
|
|
2251
|
+
if (isResponsesServiceTierRejection(failure) && !websocket.hasActiveResponse) throw new OpenAIResponsesWebSocketSafeRetryError("invalid_request_error", void 0, "service_tier", "Invalid service_tier argument", failure);
|
|
2252
|
+
outputObserved ||= !isPrelude;
|
|
2253
|
+
if (acceptance === "pending") {
|
|
2254
|
+
acceptance = "observing";
|
|
2255
|
+
await notifyProviderStreamOpened({
|
|
2256
|
+
options,
|
|
2257
|
+
cancelStream: () => websocket.finish({ keep: false })
|
|
2258
|
+
});
|
|
2259
|
+
acceptance = "accepted";
|
|
2260
|
+
}
|
|
2261
|
+
startStream();
|
|
2262
|
+
yield event;
|
|
2263
|
+
}
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
if (error instanceof OpenAIResponsesWebSocketSafeRetryError) {
|
|
2266
|
+
if (isResponsesServiceTierRejection(error)) {
|
|
2267
|
+
const nextTier = nextResponsesServiceTier(websocket.request.service_tier, error);
|
|
2268
|
+
if (!nextTier || outputObserved || acceptance === "observing" || output.content.length || websocketSignal.aborted || websocket.hasActiveResponse) throw error;
|
|
2269
|
+
closeWebSocketForFallback("service_tier_rejected");
|
|
2270
|
+
const rejectedTier = websocket.request.service_tier;
|
|
2271
|
+
if (rejectedTier === "ultrafast" || rejectedTier === "priority") responsesServiceTierObserver.reject(options, rejectedTier);
|
|
2272
|
+
yield* await createSseStream({
|
|
2273
|
+
...await buildRequest("full-history"),
|
|
2274
|
+
service_tier: nextTier
|
|
2275
|
+
});
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const encryptedContentRejected = isInvalidEncryptedContentError(error);
|
|
2279
|
+
const recovery = encryptedContentRejected ? await resolveNextResponsesEncryptedContentAttempt({
|
|
2280
|
+
kind: "initial",
|
|
2281
|
+
request: {
|
|
2282
|
+
...websocket.request,
|
|
2283
|
+
stream: true
|
|
2284
|
+
}
|
|
2285
|
+
}, error, { buildFullHistoryRequest: () => buildRequest("full-history") }) : void 0;
|
|
2286
|
+
if (encryptedContentRejected && !recovery) throw error;
|
|
2287
|
+
closeWebSocketForFallback(`safe_server_error code=${error.code} status=${safeDebugValue(error.status)} param=${safeDebugValue(error.param)}`);
|
|
2288
|
+
yield* await createSseStream(recovery?.request ?? await buildRequest("full-history"), recovery?.kind ?? "continuation-rejected", recovery?.rejectedCompaction);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
if (websocketSignal.aborted || !(error instanceof OpenAIResponsesWebSocketPreDispatchError)) throw error;
|
|
2292
|
+
params.setTransportToSse();
|
|
2293
|
+
logWebSocketFallback("before_first_event");
|
|
2294
|
+
yield* await createSseStream();
|
|
2295
|
+
}
|
|
2296
|
+
} };
|
|
2297
|
+
}
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region packages/ai/src/transports/openai-responses-websocket.ts
|
|
2300
|
+
const SESSION_WEBSOCKET_CACHE_TTL_MS = 3e5;
|
|
2301
|
+
const SESSION_WEBSOCKET_MAX_AGE_MS = 33e5;
|
|
2302
|
+
const WEBSOCKET_OPEN_STATE = 1;
|
|
2303
|
+
const websocketSessionCache = /* @__PURE__ */ new Map();
|
|
2304
|
+
const degradedWebSocketConnections = /* @__PURE__ */ new Map();
|
|
2305
|
+
function closeWebSocketSilently(socket, reason = "done") {
|
|
2306
|
+
try {
|
|
2307
|
+
socket.close({
|
|
2308
|
+
code: 1e3,
|
|
2309
|
+
reason
|
|
2310
|
+
});
|
|
2311
|
+
} catch {}
|
|
2312
|
+
}
|
|
2313
|
+
function invalidateOwnedWebSocketSession(cacheKey, entry, reason = "done") {
|
|
2314
|
+
if (entry.idleTimer) {
|
|
2315
|
+
clearTimeout(entry.idleTimer);
|
|
2316
|
+
entry.idleTimer = void 0;
|
|
2317
|
+
}
|
|
2318
|
+
closeWebSocketSilently(entry.socket, reason);
|
|
2319
|
+
entry.steeringContinuation?.steering.close(/* @__PURE__ */ new Error("Responses steering connection closed"));
|
|
2320
|
+
entry.steeringContinuation?.iterator.return?.().catch(() => void 0);
|
|
2321
|
+
entry.steeringContinuation = void 0;
|
|
2322
|
+
if (websocketSessionCache.get(cacheKey) === entry) websocketSessionCache.delete(cacheKey);
|
|
2323
|
+
}
|
|
2324
|
+
function scheduleSessionWebSocketExpiry(cacheKey, entry) {
|
|
2325
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
2326
|
+
entry.idleTimer = setTimeout(() => {
|
|
2327
|
+
if (entry.busy) return;
|
|
2328
|
+
invalidateOwnedWebSocketSession(cacheKey, entry, "idle_timeout");
|
|
2329
|
+
}, SESSION_WEBSOCKET_CACHE_TTL_MS);
|
|
2330
|
+
entry.idleTimer.unref?.();
|
|
2331
|
+
}
|
|
2332
|
+
function prepareWebSocketConnection(client, headers) {
|
|
2333
|
+
if (!isOfficialOpenAIResponsesBaseUrl(client.baseURL)) throw new Error("OpenAI Responses WebSocket requires the official API endpoint");
|
|
2334
|
+
if (typeof client.apiKey !== "string" || client.apiKey.length === 0) throw new Error("OpenAI Responses WebSocket requires an API key");
|
|
2335
|
+
const owner = getAiTransportHost();
|
|
2336
|
+
const resolvedApiKey = owner.resolveSecretSentinel(client.apiKey);
|
|
2337
|
+
const resolvedHeaders = { ...resolveAiTransportHeaderSentinels(headers) };
|
|
2338
|
+
for (const key of Object.keys(resolvedHeaders)) {
|
|
2339
|
+
const normalizedKey = key.toLowerCase();
|
|
2340
|
+
if (normalizedKey === "authorization" || normalizedKey === "traceparent") delete resolvedHeaders[key];
|
|
2341
|
+
}
|
|
2342
|
+
if (!resolvedApiKey) throw new Error("OpenAI Responses WebSocket requires a resolved API key");
|
|
2343
|
+
return {
|
|
2344
|
+
client: client.withOptions({ apiKey: resolvedApiKey }),
|
|
2345
|
+
headers: resolvedHeaders,
|
|
2346
|
+
owner,
|
|
2347
|
+
ownerId: getSessionResourceOwnerId(owner),
|
|
2348
|
+
identity: sha256Hex(JSON.stringify([
|
|
2349
|
+
resolvedApiKey,
|
|
2350
|
+
client.baseURL,
|
|
2351
|
+
Object.entries(resolvedHeaders).toSorted(([a], [b]) => a.localeCompare(b))
|
|
2352
|
+
]))
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
function createWebSocket(connection, onError) {
|
|
2356
|
+
const socket = new ResponsesWS(connection.client, {
|
|
2357
|
+
headers: connection.headers,
|
|
2358
|
+
maxQueueSize: 1
|
|
2359
|
+
});
|
|
2360
|
+
socket.on("error", () => onError(socket));
|
|
2361
|
+
return socket;
|
|
2362
|
+
}
|
|
2363
|
+
function createTransientWebSocketLease(connection) {
|
|
2364
|
+
const socket = createWebSocket(connection, (failedSocket) => closeWebSocketSilently(failedSocket, "transport_error"));
|
|
2365
|
+
return {
|
|
2366
|
+
socket,
|
|
2367
|
+
iterator: socket.stream(),
|
|
2368
|
+
reusedConnection: false,
|
|
2369
|
+
release: () => closeWebSocketSilently(socket)
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function createCachedWebSocketLease(cacheKey, entry, reusedConnection) {
|
|
2373
|
+
entry.busy = true;
|
|
2374
|
+
const steeringContinuation = entry.steeringContinuation;
|
|
2375
|
+
entry.steeringContinuation = void 0;
|
|
2376
|
+
return {
|
|
2377
|
+
socket: entry.socket,
|
|
2378
|
+
iterator: steeringContinuation?.iterator ?? entry.socket.stream(),
|
|
2379
|
+
entry,
|
|
2380
|
+
reusedConnection,
|
|
2381
|
+
steeringContinuation,
|
|
2382
|
+
release: ({ keep } = {}) => {
|
|
2383
|
+
if (!keep || entry.socket.socket.readyState !== WEBSOCKET_OPEN_STATE) {
|
|
2384
|
+
invalidateOwnedWebSocketSession(cacheKey, entry);
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
entry.busy = false;
|
|
2388
|
+
scheduleSessionWebSocketExpiry(cacheKey, entry);
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function acquireWebSocket(params, connection) {
|
|
2393
|
+
if (params.mode === "websocket" || !params.sessionId) return createTransientWebSocketLease(connection);
|
|
2394
|
+
const cacheKey = `${connection.ownerId}\0${params.sessionId}\0${connection.identity}`;
|
|
2395
|
+
const cached = websocketSessionCache.get(cacheKey);
|
|
2396
|
+
if (cached) {
|
|
2397
|
+
if (cached.idleTimer) {
|
|
2398
|
+
clearTimeout(cached.idleTimer);
|
|
2399
|
+
cached.idleTimer = void 0;
|
|
2400
|
+
}
|
|
2401
|
+
if (cached.busy) return createTransientWebSocketLease(connection);
|
|
2402
|
+
const expired = Date.now() - cached.createdAt >= SESSION_WEBSOCKET_MAX_AGE_MS;
|
|
2403
|
+
if (!expired && cached.socket.socket.readyState === WEBSOCKET_OPEN_STATE) return createCachedWebSocketLease(cacheKey, cached, true);
|
|
2404
|
+
invalidateOwnedWebSocketSession(cacheKey, cached, expired ? "connection_age_limit" : "done");
|
|
2405
|
+
}
|
|
2406
|
+
const entry = {
|
|
2407
|
+
socket: createWebSocket(connection, (failedSocket) => {
|
|
2408
|
+
const failedEntry = websocketSessionCache.get(cacheKey);
|
|
2409
|
+
if (failedEntry?.socket === failedSocket) invalidateOwnedWebSocketSession(cacheKey, failedEntry, "transport_error");
|
|
2410
|
+
else closeWebSocketSilently(failedSocket, "transport_error");
|
|
2411
|
+
}),
|
|
2412
|
+
sessionId: params.sessionId,
|
|
2413
|
+
owner: connection.owner,
|
|
2414
|
+
busy: true,
|
|
2415
|
+
createdAt: Date.now()
|
|
2416
|
+
};
|
|
2417
|
+
websocketSessionCache.set(cacheKey, entry);
|
|
2418
|
+
return createCachedWebSocketLease(cacheKey, entry, false);
|
|
2419
|
+
}
|
|
2420
|
+
function sanitizeWebSocketRequest(request) {
|
|
2421
|
+
const { stream: _stream, background: _background, ...websocketRequest } = request;
|
|
2422
|
+
return websocketRequest;
|
|
2423
|
+
}
|
|
2424
|
+
async function nextWebSocketMessage(iterator, signal) {
|
|
2425
|
+
if (signal?.aborted) throw transportAbortError(signal);
|
|
2426
|
+
return await racePromiseWithAbortSignal(iterator.next(), signal, transportAbortError);
|
|
2427
|
+
}
|
|
2428
|
+
function readServerEvent(message) {
|
|
2429
|
+
if (message.type === "message") return message.message;
|
|
2430
|
+
if (message.type === "error") throw parseOpenAIResponsesWebSocketServerError(message.error) ?? new Error("OpenAI Responses WebSocket transport failed", { cause: message.error });
|
|
2431
|
+
if (message.type === "close") throw new Error(`OpenAI Responses WebSocket closed before completion (code ${message.code})`);
|
|
2432
|
+
}
|
|
2433
|
+
function createOpenAIResponsesWebSocketStream(params) {
|
|
2434
|
+
const connection = prepareWebSocketConnection(params.client, params.headers);
|
|
2435
|
+
let fullRequest = sanitizeWebSocketRequest(params.request);
|
|
2436
|
+
const requestModel = typeof fullRequest.model === "string" ? fullRequest.model : "";
|
|
2437
|
+
const degradationKey = `${connection.ownerId}\0${params.sessionId ?? ""}\0${connection.identity}\0${requestModel}`;
|
|
2438
|
+
const degraded = degradedWebSocketConnections.get(degradationKey);
|
|
2439
|
+
if (degraded && degraded.retryAt > Date.now()) throw new OpenAIResponsesWebSocketPreDispatchError(/* @__PURE__ */ new Error("OpenAI Responses WebSocket is cooling down after a transport failure"));
|
|
2440
|
+
if (degraded) clearTimeout(degraded.expiryTimer);
|
|
2441
|
+
degradedWebSocketConnections.delete(degradationKey);
|
|
2442
|
+
const markDegraded = () => {
|
|
2443
|
+
const cooldownMs = params.degradeCooldownMs;
|
|
2444
|
+
if (cooldownMs === void 0 || !Number.isFinite(cooldownMs) || cooldownMs <= 0) return;
|
|
2445
|
+
const entry = {
|
|
2446
|
+
sessionId: params.sessionId,
|
|
2447
|
+
owner: connection.owner,
|
|
2448
|
+
retryAt: Date.now() + cooldownMs,
|
|
2449
|
+
expiryTimer: setTimeout(() => {
|
|
2450
|
+
if (degradedWebSocketConnections.get(degradationKey) === entry) degradedWebSocketConnections.delete(degradationKey);
|
|
2451
|
+
}, cooldownMs)
|
|
2452
|
+
};
|
|
2453
|
+
entry.expiryTimer.unref?.();
|
|
2454
|
+
degradedWebSocketConnections.set(degradationKey, entry);
|
|
2455
|
+
};
|
|
2456
|
+
let lease;
|
|
2457
|
+
try {
|
|
2458
|
+
lease = acquireWebSocket(params, connection);
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
markDegraded();
|
|
2461
|
+
throw new OpenAIResponsesWebSocketPreDispatchError(error);
|
|
2462
|
+
}
|
|
2463
|
+
let prepared;
|
|
2464
|
+
let previousContinuation;
|
|
2465
|
+
const resumedSteering = lease.steeringContinuation;
|
|
2466
|
+
const steeringMode = resumedSteering ? resumedSteering.requiresInput ? "required-input" : "automatic" : void 0;
|
|
2467
|
+
try {
|
|
2468
|
+
const continuation = previousContinuation = lease.entry?.continuation;
|
|
2469
|
+
if (continuation && lease.entry) {
|
|
2470
|
+
lease.entry.continuation = void 0;
|
|
2471
|
+
prepared = resolveResponsesContinuationRequest(continuation, fullRequest, steeringMode);
|
|
2472
|
+
} else {
|
|
2473
|
+
fullRequest = params.restoreRequest?.(fullRequest) ?? fullRequest;
|
|
2474
|
+
prepared = {
|
|
2475
|
+
request: fullRequest,
|
|
2476
|
+
continuationStatus: lease.entry ? "no_previous_response" : "socket_not_cached"
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
lease.iterator.return?.().catch(() => void 0);
|
|
2481
|
+
lease.release({ keep: false });
|
|
2482
|
+
throw error;
|
|
2483
|
+
}
|
|
2484
|
+
let streamStarted = false;
|
|
2485
|
+
let terminalResponse;
|
|
2486
|
+
let terminalReceived = false;
|
|
2487
|
+
let released = false;
|
|
2488
|
+
let retainIterator = false;
|
|
2489
|
+
let deferredInput = false;
|
|
2490
|
+
let inputReplay;
|
|
2491
|
+
if (resumedSteering) try {
|
|
2492
|
+
if (prepared.continuationStatus !== "continued" || !prepared.request.previous_response_id) throw new Error("Responses steering continuation changed its request or history");
|
|
2493
|
+
const input = omitAcceptedSteering(prepared.request.input ?? [], resumedSteering.acceptedInput);
|
|
2494
|
+
if (!resumedSteering.requiresInput && (stableStringify(fullRequest.instructions) !== stableStringify(resumedSteering.instructions) || stableStringify(fullRequest.tools) !== stableStringify(resumedSteering.tools))) throw new Error("Responses automatic steering continuation cannot change instructions or tools");
|
|
2495
|
+
const baseline = prepared.fullRequest ?? fullRequest;
|
|
2496
|
+
const priorLength = (baseline.input?.length ?? 0) - (prepared.request.input?.length ?? 0);
|
|
2497
|
+
const fingerprints = input.map(responsesInputFingerprint);
|
|
2498
|
+
if (fingerprints.length > 0) inputReplay = {
|
|
2499
|
+
afterResponseId: prepared.request.previous_response_id,
|
|
2500
|
+
before: resumedSteering.requiresInput ? fingerprints : [],
|
|
2501
|
+
after: resumedSteering.requiresInput ? [] : fingerprints
|
|
2502
|
+
};
|
|
2503
|
+
const delivered = resumedSteering.requiresInput ? input : [];
|
|
2504
|
+
prepared.fullRequest = {
|
|
2505
|
+
...baseline,
|
|
2506
|
+
input: [
|
|
2507
|
+
...(baseline.input ?? []).slice(0, priorLength),
|
|
2508
|
+
...resumedSteering.acceptedInput,
|
|
2509
|
+
...delivered
|
|
2510
|
+
]
|
|
2511
|
+
};
|
|
2512
|
+
prepared.request = {
|
|
2513
|
+
...prepared.request,
|
|
2514
|
+
input: delivered
|
|
2515
|
+
};
|
|
2516
|
+
deferredInput = !resumedSteering.requiresInput && input.length > 0;
|
|
2517
|
+
} catch (error) {
|
|
2518
|
+
lease.iterator.return?.().catch(() => void 0);
|
|
2519
|
+
lease.release({ keep: false });
|
|
2520
|
+
throw new OpenAIResponsesWebSocketPostDispatchError(error);
|
|
2521
|
+
}
|
|
2522
|
+
const steering = lease.entry && params.onActiveResponse && params.steeringInput ? createResponsesSteering({
|
|
2523
|
+
onActiveResponse: params.onActiveResponse,
|
|
2524
|
+
toInput: params.steeringInput,
|
|
2525
|
+
send: (event) => lease.socket.sendRaw(JSON.stringify(event)),
|
|
2526
|
+
needsContinuation: () => deferredInput,
|
|
2527
|
+
assertActive: () => {
|
|
2528
|
+
if (released || terminalReceived || params.signal?.aborted) throw new Error("Responses steering is no longer active");
|
|
2529
|
+
}
|
|
2530
|
+
}) : void 0;
|
|
2531
|
+
const finish = ({ keep = true } = {}) => {
|
|
2532
|
+
if (released) return;
|
|
2533
|
+
released = true;
|
|
2534
|
+
if (keep && lease.entry && terminalResponse) lease.entry.continuation = recordResponsesContinuationState(previousContinuation, prepared.fullRequest ?? fullRequest, terminalResponse, previousContinuation?.lastResponseId === prepared.request.previous_response_id);
|
|
2535
|
+
lease.release({ keep });
|
|
2536
|
+
};
|
|
2537
|
+
return {
|
|
2538
|
+
stream: { async *[Symbol.asyncIterator]() {
|
|
2539
|
+
if (streamStarted) throw new Error("OpenAI Responses WebSocket stream can only be consumed once");
|
|
2540
|
+
streamStarted = true;
|
|
2541
|
+
const iterator = lease.iterator;
|
|
2542
|
+
let requestDispatched = false;
|
|
2543
|
+
try {
|
|
2544
|
+
if (params.signal?.aborted) throw transportAbortError(params.signal);
|
|
2545
|
+
if (resumedSteering) {
|
|
2546
|
+
requestDispatched = true;
|
|
2547
|
+
if (resumedSteering.requiresInput) lease.socket.send({
|
|
2548
|
+
...prepared.request,
|
|
2549
|
+
type: "response.create"
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
for (;;) {
|
|
2553
|
+
const buffered = resumedSteering?.buffered.shift();
|
|
2554
|
+
const next = buffered ? {
|
|
2555
|
+
value: buffered,
|
|
2556
|
+
done: false
|
|
2557
|
+
} : await nextWebSocketMessage(iterator, params.signal);
|
|
2558
|
+
if (next.done) throw new Error("OpenAI Responses WebSocket closed before a terminal response event");
|
|
2559
|
+
if (next.value.type === "open") {
|
|
2560
|
+
if (!requestDispatched) {
|
|
2561
|
+
requestDispatched = true;
|
|
2562
|
+
lease.socket.send({
|
|
2563
|
+
...prepared.request,
|
|
2564
|
+
type: "response.create"
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
const event = readServerEvent(next.value);
|
|
2570
|
+
if (!event) continue;
|
|
2571
|
+
if (isRecord(event) && typeof event.type === "string" && event.type.startsWith("response.steer.")) {
|
|
2572
|
+
if ((isRecord(event.steer) && event.steer.previous_response_id === steering?.responseId ? steering : resumedSteering?.steering ?? steering)?.handle(event)) continue;
|
|
2573
|
+
}
|
|
2574
|
+
steering?.handle(event);
|
|
2575
|
+
if (event.type === "response.completed") terminalResponse = event.response;
|
|
2576
|
+
terminalReceived = event.type === "response.completed" || event.type === "response.incomplete" || event.type === "response.failed";
|
|
2577
|
+
if (terminalReceived) {
|
|
2578
|
+
steering?.seal();
|
|
2579
|
+
if (event.type === "response.failed") steering?.close(/* @__PURE__ */ new Error("Responses failed before steering could be applied"));
|
|
2580
|
+
const continuationBuffer = [];
|
|
2581
|
+
for (;;) {
|
|
2582
|
+
if (!steering?.pending) break;
|
|
2583
|
+
const acknowledgement = await nextWebSocketMessage(iterator, params.signal);
|
|
2584
|
+
if (acknowledgement.done) throw new Error("Responses closed before acknowledging steering");
|
|
2585
|
+
const acknowledgedEvent = readServerEvent(acknowledgement.value);
|
|
2586
|
+
if (!steering.handle(acknowledgedEvent)) continuationBuffer.push(acknowledgement.value);
|
|
2587
|
+
}
|
|
2588
|
+
const acceptedInput = steering?.acceptedInput ?? [];
|
|
2589
|
+
const isSteered = event.type === "response.incomplete" && isRecord(event.response.incomplete_details) && event.response.incomplete_details.reason === "steered";
|
|
2590
|
+
if (lease.entry && steering && acceptedInput.length > 0 && (event.type === "response.completed" || isSteered)) {
|
|
2591
|
+
if (event.type === "response.incomplete") terminalResponse = event.response;
|
|
2592
|
+
lease.entry.steeringContinuation = {
|
|
2593
|
+
iterator,
|
|
2594
|
+
buffered: continuationBuffer,
|
|
2595
|
+
acceptedInput,
|
|
2596
|
+
requiresInput: (terminalResponse?.output ?? []).some((item) => (item.type === "function_call" || item.type === "custom_tool_call") && !(isRecord(item) && item.async === true) || item.type === "mcp_approval_request"),
|
|
2597
|
+
steering,
|
|
2598
|
+
instructions: fullRequest.instructions,
|
|
2599
|
+
tools: fullRequest.tools
|
|
2600
|
+
};
|
|
2601
|
+
retainIterator = true;
|
|
2602
|
+
if (isSteered) {
|
|
2603
|
+
yield {
|
|
2604
|
+
...event,
|
|
2605
|
+
type: "response.completed",
|
|
2606
|
+
response: {
|
|
2607
|
+
...event.response,
|
|
2608
|
+
status: "completed",
|
|
2609
|
+
incomplete_details: null
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
yield event;
|
|
2617
|
+
if (terminalReceived) {
|
|
2618
|
+
degradedWebSocketConnections.delete(degradationKey);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
} catch (error) {
|
|
2623
|
+
const steeringDispatched = Boolean(steering?.pending || steering?.acceptedInput.length || resumedSteering);
|
|
2624
|
+
steering?.close(error instanceof Error ? error : /* @__PURE__ */ new Error("Responses steering failed"));
|
|
2625
|
+
if (lease.entry) lease.entry.continuation = void 0;
|
|
2626
|
+
const safeRetry = error instanceof OpenAIResponsesWebSocketSafeRetryError && !steeringDispatched;
|
|
2627
|
+
if (!params.callerSignal?.aborted && !safeRetry) markDegraded();
|
|
2628
|
+
if (!requestDispatched && !params.signal?.aborted) throw new OpenAIResponsesWebSocketPreDispatchError(error);
|
|
2629
|
+
if (!requestDispatched || params.callerSignal?.aborted || safeRetry) throw error;
|
|
2630
|
+
throw new OpenAIResponsesWebSocketPostDispatchError(error);
|
|
2631
|
+
} finally {
|
|
2632
|
+
steering?.seal();
|
|
2633
|
+
if (!retainIterator) {
|
|
2634
|
+
steering?.close(/* @__PURE__ */ new Error("Responses stream ended before steering was confirmed"));
|
|
2635
|
+
await iterator.return?.().catch(() => void 0);
|
|
2636
|
+
}
|
|
2637
|
+
if (!terminalReceived) finish({ keep: false });
|
|
2638
|
+
}
|
|
2639
|
+
} },
|
|
2640
|
+
request: prepared.request,
|
|
2641
|
+
fullRequest: prepared.fullRequest ?? fullRequest,
|
|
2642
|
+
reusedConnection: lease.reusedConnection,
|
|
2643
|
+
continuationStatus: prepared.continuationStatus,
|
|
2644
|
+
inputReplay,
|
|
2645
|
+
get hasActiveResponse() {
|
|
2646
|
+
return Boolean(steering?.responseId || resumedSteering);
|
|
2647
|
+
},
|
|
2648
|
+
finish
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
function closeOpenAIResponsesWebSocketSessions(sessionId, owner) {
|
|
2652
|
+
for (const [cacheKey, entry] of websocketSessionCache) {
|
|
2653
|
+
if (owner && entry.owner !== owner || sessionId && entry.sessionId !== sessionId) continue;
|
|
2654
|
+
invalidateOwnedWebSocketSession(cacheKey, entry, "session_cleanup");
|
|
2655
|
+
}
|
|
2656
|
+
for (const [key, entry] of degradedWebSocketConnections) if ((!owner || entry.owner === owner) && (!sessionId || entry.sessionId === sessionId)) {
|
|
2657
|
+
clearTimeout(entry.expiryTimer);
|
|
2658
|
+
degradedWebSocketConnections.delete(key);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
registerSessionResourceCleanup(closeOpenAIResponsesWebSocketSessions);
|
|
2662
|
+
//#endregion
|
|
2663
|
+
//#region packages/ai/src/transports/openai-responses-client.ts
|
|
2664
|
+
function resolveNativeOpenAIResponsesWebSocketMode(model, transport) {
|
|
2665
|
+
if (transport !== "websocket" && transport !== "websocket-cached" && transport !== "auto") return;
|
|
2666
|
+
if (getAiTransportHost().requiresManagedTransport(model)) return;
|
|
2667
|
+
return supportsNativeOpenAIResponsesEndpoint(model) ? transport : void 0;
|
|
2668
|
+
}
|
|
2669
|
+
function combineWebSocketTimeoutSignal(signal, model, timeoutMs) {
|
|
2670
|
+
const resolvedTimeoutMs = timeoutMs !== void 0 && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : getAiTransportHost().resolveModelRequestTimeoutMs(model);
|
|
2671
|
+
if (resolvedTimeoutMs === void 0 || !Number.isFinite(resolvedTimeoutMs)) return signal;
|
|
2672
|
+
return AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, resolvedTimeoutMs))]);
|
|
2673
|
+
}
|
|
2674
|
+
function createOpenAIResponsesClient(model, apiKey, defaultHeaders, fetchOverride) {
|
|
2675
|
+
return new OpenAI({
|
|
2676
|
+
apiKey,
|
|
2677
|
+
baseURL: resolveOpenAIClientBaseUrl(model),
|
|
2678
|
+
dangerouslyAllowBrowser: true,
|
|
2679
|
+
defaultHeaders,
|
|
2680
|
+
fetch: fetchOverride ?? buildGuardedModelFetch(model),
|
|
2681
|
+
...buildOpenAISdkClientOptions(model)
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
function createResponsesTransportExecutor(config) {
|
|
2685
|
+
return (model, context, options) => {
|
|
2686
|
+
const responsesOptions = options;
|
|
2687
|
+
const compactRequest = claimResponsesCompactRequest(responsesOptions);
|
|
2688
|
+
const { eventStream, stream } = createWritableTransportEventStream();
|
|
2689
|
+
(async () => {
|
|
2690
|
+
const output = createAssistantOutput(model, config.outputApi);
|
|
2691
|
+
let firstEventAbort;
|
|
2692
|
+
let continuationClaim;
|
|
2693
|
+
const requestLifecycle = responsesRequestLifecycle.get(options);
|
|
2694
|
+
try {
|
|
2695
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
2696
|
+
const websocketMode = resolveNativeOpenAIResponsesWebSocketMode(model, responsesOptions?.transport);
|
|
2697
|
+
const encodeBody = prepareModelRequestBody(websocketMode || compactRequest ? void 0 : options);
|
|
2698
|
+
const turnState = resolveProviderTransportTurnState(model, {
|
|
2699
|
+
sessionId: options?.sessionId,
|
|
2700
|
+
turnId: randomUUID(),
|
|
2701
|
+
attempt: 1,
|
|
2702
|
+
transport: websocketMode ? "websocket" : "stream"
|
|
2703
|
+
});
|
|
2704
|
+
const websocketSessionPolicy = websocketMode ? turnState?.websocket : void 0;
|
|
2705
|
+
const httpTurnHeaders = filterProviderTurnHeadersForExplicitOpencodeSession(model, options, turnState?.headers);
|
|
2706
|
+
const websocketTurnHeaders = filterProviderTurnHeadersForExplicitOpencodeSession(model, options, websocketSessionPolicy?.headers);
|
|
2707
|
+
const websocketHeaders = websocketMode ? buildOpenAIClientHeaders(model, context, options?.headers, websocketTurnHeaders, options?.sessionId, options?.cacheRetention) : void 0;
|
|
2708
|
+
const httpHeaders = buildOpenAIClientHeaders(model, context, options?.headers, httpTurnHeaders, options?.sessionId, options?.cacheRetention);
|
|
2709
|
+
const fetchOverride = createResponsesRequestFetch(model, {
|
|
2710
|
+
compact: Boolean(compactRequest),
|
|
2711
|
+
stream: config.streamRequest,
|
|
2712
|
+
lifecycle: requestLifecycle,
|
|
2713
|
+
onSseComment: () => notifyLlmRequestActivity(options?.signal, false)
|
|
2714
|
+
});
|
|
2715
|
+
const client = config.createClient(model, apiKey, httpHeaders, fetchOverride);
|
|
2716
|
+
const nativeAstra = model.id === "gpt-6-astra" && supportsNativeOpenAIResponsesEndpoint(model);
|
|
2717
|
+
const asyncToolExecutionEligible = nativeAstra && options?.asyncToolExecution === true && !responsesOptions?.openclawCodeModeToolSurface;
|
|
2718
|
+
const prepareRequest = async (request) => {
|
|
2719
|
+
let params = request;
|
|
2720
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
2721
|
+
if (nextParams !== void 0) params = nextParams;
|
|
2722
|
+
params = sanitizeOpenAICodexResponsesParams(model, params);
|
|
2723
|
+
params = sanitizeResponsesImagePayload(params);
|
|
2724
|
+
if (responsesOptions?.openclawCodeModeToolSurface === true) {
|
|
2725
|
+
const visibleToolNames = resolveCodeModeResponsesVisibleToolNames(context);
|
|
2726
|
+
const allowedHostedToolTypes = responsesOptions?.openclawCodeModeAllowedHostedToolTypes;
|
|
2727
|
+
enforceCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes, codeModeToolSurfaceObserver.get(options));
|
|
2728
|
+
assertCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes);
|
|
2729
|
+
}
|
|
2730
|
+
if (asyncToolExecutionEligible && params.model === "gpt-6-astra" && params.multi_agent?.enabled !== true && params.tools) {
|
|
2731
|
+
const synchronousTools = new Set(context.tools?.flatMap((tool) => tool.async === false ? [tool.name] : []));
|
|
2732
|
+
params.tools = params.tools.map((tool) => tool.type === "function" && !synchronousTools.has(tool.name) ? {
|
|
2733
|
+
...tool,
|
|
2734
|
+
async: true
|
|
2735
|
+
} : tool);
|
|
2736
|
+
}
|
|
2737
|
+
return params;
|
|
2738
|
+
};
|
|
2739
|
+
const buildRequest = (replayMode, requestContext = context) => prepareRequest(config.buildRequest(model, requestContext, responsesOptions, turnState?.metadata, replayMode));
|
|
2740
|
+
let params = await buildRequest("checkpoint");
|
|
2741
|
+
const asyncTools = asyncToolExecutionEligible && params.model === "gpt-6-astra" && params.multi_agent?.enabled !== true;
|
|
2742
|
+
if (compactRequest) {
|
|
2743
|
+
const compacted = await postOpenAIResponsesCompaction({
|
|
2744
|
+
client,
|
|
2745
|
+
model,
|
|
2746
|
+
request: params,
|
|
2747
|
+
options: responsesOptions
|
|
2748
|
+
});
|
|
2749
|
+
output.usage.input = compacted.usage.input_tokens;
|
|
2750
|
+
output.usage.output = compacted.usage.output_tokens;
|
|
2751
|
+
output.usage.totalTokens = compacted.usage.input_tokens + compacted.usage.output_tokens;
|
|
2752
|
+
compactRequest.resolve(compacted);
|
|
2753
|
+
finalizeTransportStream({
|
|
2754
|
+
stream,
|
|
2755
|
+
output,
|
|
2756
|
+
signal: options?.signal
|
|
2757
|
+
});
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
const sessionId = options?.sessionId;
|
|
2761
|
+
if (config.httpContinuation && !websocketMode && !getAiTransportHost().requiresManagedTransport(model) && (supportsNativeOpenAIResponsesEndpoint(model) || resolveOpenAIResponsesPayloadPolicy(model).explicitContinuationOptIn) && sessionId && (params.store === true || supportsResponsesReasoningUpdate(params)) && !params.previous_response_id) {
|
|
2762
|
+
continuationClaim = claimOpenAIResponsesHttpContinuation({
|
|
2763
|
+
sessionId,
|
|
2764
|
+
apiKey,
|
|
2765
|
+
baseUrl: model.baseUrl,
|
|
2766
|
+
headers: httpHeaders,
|
|
2767
|
+
request: params,
|
|
2768
|
+
restoreRequest: () => restoreResponsesReasoningState(context, model, responsesOptions, params)
|
|
2769
|
+
});
|
|
2770
|
+
if (continuationClaim) params = continuationClaim.fullRequest;
|
|
2771
|
+
}
|
|
2772
|
+
const observePrompt = createResponsesPromptEgressObserver(responsesOptions, context.systemPrompt);
|
|
2773
|
+
const requestStartedAt = Date.now();
|
|
2774
|
+
let started = false;
|
|
2775
|
+
const startStream = () => {
|
|
2776
|
+
if (!started) {
|
|
2777
|
+
started = true;
|
|
2778
|
+
stream.push({
|
|
2779
|
+
type: "start",
|
|
2780
|
+
partial: output
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
const firstEvent = createFirstStreamEventAbortController(options?.signal);
|
|
2785
|
+
firstEventAbort = firstEvent;
|
|
2786
|
+
const requestOptions = buildOpenAISdkRequestOptions(model, firstEvent.signal, {
|
|
2787
|
+
stream: config.streamRequest,
|
|
2788
|
+
timeoutMs: options?.timeoutMs
|
|
2789
|
+
});
|
|
2790
|
+
const websocketSignal = combineWebSocketTimeoutSignal(firstEvent.signal, model, requestOptions?.timeout);
|
|
2791
|
+
emitModelTransportDebug(log, `[responses] start provider=${model.provider} api=${model.api} model=${model.id} requestIdHash=${redactIdentifier(options?.requestId, { len: 64 })} baseUrl=${formatModelTransportDebugBaseUrl(model.baseUrl)} timeoutMs=${safeDebugValue(requestOptions?.timeout)} apiKey=${apiKey ? "present" : "missing"} ${summarizeResponsesPayload(params)}`);
|
|
2792
|
+
const responseModelTracker = createResponseModelTracker(isOpenAICodexResponsesModel(model));
|
|
2793
|
+
let continuationBaseline;
|
|
2794
|
+
let dispatchedPreviousResponseId;
|
|
2795
|
+
let contextUsageEligible = true;
|
|
2796
|
+
let requestedTier;
|
|
2797
|
+
const createSseStream = async (initialRequest = continuationClaim?.request ?? params, initialAttemptKind = "initial", initialRejectedCompaction) => {
|
|
2798
|
+
const { stream: responseStream } = await createResponsesStreamWithRecovery({
|
|
2799
|
+
client,
|
|
2800
|
+
request: initialRequest,
|
|
2801
|
+
requestOptions,
|
|
2802
|
+
model,
|
|
2803
|
+
encodeBody,
|
|
2804
|
+
observePrompt,
|
|
2805
|
+
initialAttemptKind,
|
|
2806
|
+
initialRejectedCompaction,
|
|
2807
|
+
buildFullHistoryRequest: () => buildRequest("full-history"),
|
|
2808
|
+
onCompactionRejected: (checkpoint) => suppressOpenAIResponsesCompaction(output, model, responsesOptions, checkpoint),
|
|
2809
|
+
canRetryStream: () => output.content.length === 0,
|
|
2810
|
+
onServiceTierRejected: (tier) => responsesServiceTierObserver.reject(options, tier),
|
|
2811
|
+
wrapStream: ({ stream: rawResponseStream, response, attempt }) => {
|
|
2812
|
+
requestedTier = attempt.request.service_tier;
|
|
2813
|
+
contextUsageEligible &&= attempt.kind === "initial";
|
|
2814
|
+
dispatchedPreviousResponseId = attempt.request.previous_response_id;
|
|
2815
|
+
continuationBaseline = attempt.request.previous_response_id ? params : attempt.request;
|
|
2816
|
+
const trackedResponseStream = responseModelTracker.track(response, rawResponseStream);
|
|
2817
|
+
return withProviderResponseHook({
|
|
2818
|
+
stream: observeResponsesStream(trackedResponseStream, model, requestStartedAt),
|
|
2819
|
+
signal: firstEvent.signal,
|
|
2820
|
+
abort: firstEvent.abort,
|
|
2821
|
+
hook: createOpenAIProviderAcceptanceHook(options, response, model),
|
|
2822
|
+
onReady: () => {
|
|
2823
|
+
emitModelTransportDebug(log, `[responses] headers provider=${model.provider} api=${model.api} model=${model.id} transport=sse elapsedMs=${Date.now() - requestStartedAt}`);
|
|
2824
|
+
startStream();
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
return responseStream;
|
|
2830
|
+
};
|
|
2831
|
+
let responseStream;
|
|
2832
|
+
let websocketBaseline;
|
|
2833
|
+
let finishWebSocket;
|
|
2834
|
+
let transport = "sse";
|
|
2835
|
+
const logWebSocketFallback = (reason) => emitModelTransportDebug(log, `[responses] websocket_fallback provider=${model.provider} api=${model.api} model=${model.id} reason=${reason}`);
|
|
2836
|
+
const closeWebSocketForFallback = (reason) => {
|
|
2837
|
+
finishWebSocket?.({ keep: false });
|
|
2838
|
+
finishWebSocket = void 0;
|
|
2839
|
+
transport = "sse";
|
|
2840
|
+
logWebSocketFallback(reason);
|
|
2841
|
+
};
|
|
2842
|
+
if (websocketMode) try {
|
|
2843
|
+
const websocket = createOpenAIResponsesWebSocketStream({
|
|
2844
|
+
client,
|
|
2845
|
+
request: params,
|
|
2846
|
+
restoreRequest: (request) => restoreResponsesReasoningState(context, model, responsesOptions, request),
|
|
2847
|
+
mode: websocketMode,
|
|
2848
|
+
sessionId: options?.sessionId,
|
|
2849
|
+
headers: websocketHeaders,
|
|
2850
|
+
signal: websocketSignal,
|
|
2851
|
+
callerSignal: options?.signal,
|
|
2852
|
+
degradeCooldownMs: websocketSessionPolicy?.degradeCooldownMs,
|
|
2853
|
+
onActiveResponse: nativeAstra && params.model === "gpt-6-astra" ? options?.onActiveResponse : void 0,
|
|
2854
|
+
steeringInput: (messages) => {
|
|
2855
|
+
contextUsageEligible = false;
|
|
2856
|
+
return projectResponsesSteeringInput(params, () => buildRequest("checkpoint", {
|
|
2857
|
+
...context,
|
|
2858
|
+
messages: [...context.messages, ...messages]
|
|
2859
|
+
}));
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2862
|
+
finishWebSocket = websocket.finish;
|
|
2863
|
+
requestedTier = websocket.request.service_tier;
|
|
2864
|
+
websocketBaseline = websocket.fullRequest;
|
|
2865
|
+
recordResponsesInputReplay(output, websocket.inputReplay);
|
|
2866
|
+
contextUsageEligible &&= websocket.inputReplay === void 0;
|
|
2867
|
+
observePrompt?.(websocket.request, {
|
|
2868
|
+
egress: "responses-websocket",
|
|
2869
|
+
payloadVariant: "initial"
|
|
2870
|
+
});
|
|
2871
|
+
transport = "websocket";
|
|
2872
|
+
emitModelTransportDebug(log, `[responses] websocket_selected provider=${model.provider} api=${model.api} model=${model.id} mode=${websocketMode} reused=${websocket.reusedConnection} continuation=${websocket.continuationStatus === "continued"} continuationStatus=${websocket.continuationStatus} sessionIdHash=${redactIdentifier(options?.sessionId)} headersHash=${redactIdentifier(JSON.stringify(Object.entries(websocketHeaders ?? {}).toSorted(([a], [b]) => a.localeCompare(b))))}`);
|
|
2873
|
+
responseStream = createRecoverableResponsesWebSocketStream({
|
|
2874
|
+
trackedWebSocketStream: responseModelTracker.track(void 0, websocket.stream),
|
|
2875
|
+
websocket,
|
|
2876
|
+
websocketSignal,
|
|
2877
|
+
options,
|
|
2878
|
+
output,
|
|
2879
|
+
buildRequest,
|
|
2880
|
+
createSseStream,
|
|
2881
|
+
closeWebSocketForFallback,
|
|
2882
|
+
logWebSocketFallback,
|
|
2883
|
+
startStream,
|
|
2884
|
+
setTransportToSse: () => {
|
|
2885
|
+
transport = "sse";
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
if (error instanceof OpenAIResponsesWebSocketPostDispatchError) throw error;
|
|
2890
|
+
closeWebSocketForFallback("setup_failure");
|
|
2891
|
+
responseStream = await createSseStream();
|
|
2892
|
+
}
|
|
2893
|
+
else responseStream = await createSseStream();
|
|
2894
|
+
try {
|
|
2895
|
+
const acceptedResponseStream = withResponsesRequestAcceptance(responseStream, requestLifecycle, transport === "websocket" ? websocketSignal : firstEvent.signal);
|
|
2896
|
+
const terminal = await processResponsesStream(acceptedResponseStream, output, stream, model, {
|
|
2897
|
+
canRetryIdentityConflict: () => hasOnlyResponsesFunctionTools(transport === "websocket" ? websocketBaseline : continuationBaseline),
|
|
2898
|
+
...config.pricingOptions?.(responsesOptions, model),
|
|
2899
|
+
resolveServiceTier: (responseTier, originalTier) => responseTier ?? (requestedTier === "priority" || requestedTier === "default" ? requestedTier : originalTier),
|
|
2900
|
+
onServiceTier: (responseTier) => responsesServiceTierObserver.observe(options, requestedTier, responseTier),
|
|
2901
|
+
firstEventTimeoutMs: getFirstStreamEventTimeoutMs(options) ?? config.firstEventTimeoutMs,
|
|
2902
|
+
abortFirstEventStream: firstEvent.abort,
|
|
2903
|
+
onFirstEventTimeout: getFirstStreamEventTimeoutHandler(options),
|
|
2904
|
+
signal: options?.signal,
|
|
2905
|
+
reasoningReplayMetadata: buildOpenAIResponsesReasoningReplayMetadata(model, {
|
|
2906
|
+
authProfileId: responsesOptions?.authProfileId,
|
|
2907
|
+
sessionId: options?.sessionId
|
|
2908
|
+
}),
|
|
2909
|
+
asyncToolExecution: asyncTools,
|
|
2910
|
+
...responseModelTracker.terminalOptions
|
|
2911
|
+
});
|
|
2912
|
+
finishWebSocket?.();
|
|
2913
|
+
if (options?.signal?.aborted) throw transportAbortError(options.signal);
|
|
2914
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") throw new Error(output.errorMessage ?? "An unknown error occurred");
|
|
2915
|
+
const admitted = transport === "websocket" ? websocketBaseline : continuationBaseline;
|
|
2916
|
+
if (terminal && admitted && supportsNativeOpenAIResponsesEndpoint(model)) recordResponsesReasoningState(output, model, responsesOptions, admitted, terminal.output);
|
|
2917
|
+
if (continuationClaim && continuationBaseline && terminal) continuationClaim.commit(continuationBaseline, terminal, dispatchedPreviousResponseId);
|
|
2918
|
+
if (terminal && admitted && contextUsageEligible) recordResponsesContextUsage(output, model, responsesOptions, admitted, terminal.output, "transport");
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
finishWebSocket?.({ keep: false });
|
|
2921
|
+
throw error;
|
|
2922
|
+
}
|
|
2923
|
+
emitModelTransportDebug(log, `[responses] completed provider=${model.provider} api=${model.api} model=${model.id} transport=${transport} elapsedMs=${Date.now() - requestStartedAt}`);
|
|
2924
|
+
finalizeTransportStream({
|
|
2925
|
+
stream,
|
|
2926
|
+
output,
|
|
2927
|
+
signal: options?.signal
|
|
2928
|
+
});
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
if (requestLifecycle) await requestLifecycle.settle().catch(() => void 0);
|
|
2931
|
+
if (compactRequest) {
|
|
2932
|
+
compactRequest.reject(error);
|
|
2933
|
+
failTransportStream({
|
|
2934
|
+
stream,
|
|
2935
|
+
output,
|
|
2936
|
+
signal: options?.signal,
|
|
2937
|
+
error
|
|
2938
|
+
});
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (error instanceof ResponsesStreamFailure && error.observation) logResponsesFailedNoDetails(error.observation);
|
|
2942
|
+
const incompleteReason = output.diagnostics?.find(({ type }) => type === "openai_responses_terminal")?.details?.incompleteReason;
|
|
2943
|
+
emitModelTransportError(log, "responses", `provider=${model.provider} api=${model.api} model=${model.id} ` + summarizeOpenAITransportError(error) + (typeof incompleteReason === "string" ? ` incompleteReason=${incompleteReason}` : ""), options?.signal);
|
|
2944
|
+
failTransportStream({
|
|
2945
|
+
stream,
|
|
2946
|
+
output,
|
|
2947
|
+
signal: options?.signal,
|
|
2948
|
+
error
|
|
2949
|
+
});
|
|
2950
|
+
} finally {
|
|
2951
|
+
continuationClaim?.release();
|
|
2952
|
+
firstEventAbort?.dispose();
|
|
2953
|
+
}
|
|
2954
|
+
})();
|
|
2955
|
+
return eventStream;
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function createOpenAIResponsesTransportStreamFn() {
|
|
2959
|
+
return createResponsesTransportExecutor({
|
|
2960
|
+
streamRequest: true,
|
|
2961
|
+
httpContinuation: true,
|
|
2962
|
+
createClient: createOpenAIResponsesClient,
|
|
2963
|
+
buildRequest: buildOpenAIResponsesParams,
|
|
2964
|
+
pricingOptions: (options, model) => ({
|
|
2965
|
+
serviceTier: options?.serviceTier,
|
|
2966
|
+
applyServiceTierPricing: (usage, serviceTier) => applyResponsesServiceTierPricing(usage, serviceTier, model)
|
|
2967
|
+
})
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
function createAzureOpenAIResponsesTransportStreamFn() {
|
|
2971
|
+
return createResponsesTransportExecutor({
|
|
2972
|
+
outputApi: "azure-openai-responses",
|
|
2973
|
+
firstEventTimeoutMs: AZURE_RESPONSES_FIRST_EVENT_TIMEOUT_MS,
|
|
2974
|
+
createClient: createAzureOpenAIClient,
|
|
2975
|
+
buildRequest: (model, context, options, metadata, replayMode) => {
|
|
2976
|
+
const deploymentName = resolveAzureDeploymentNameFromMap({
|
|
2977
|
+
modelId: model.id,
|
|
2978
|
+
deploymentMap: process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP
|
|
2979
|
+
});
|
|
2980
|
+
const params = buildOpenAIResponsesParams(model, context, options, metadata, replayMode);
|
|
2981
|
+
params.model = deploymentName;
|
|
2982
|
+
delete params.store;
|
|
2983
|
+
return params;
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
function createAzureOpenAIClient(model, apiKey, defaultHeaders, fetchOverride) {
|
|
2988
|
+
const baseURL = model.baseUrl.replace(/\/+$/, "");
|
|
2989
|
+
const clientOptions = {
|
|
2990
|
+
apiKey,
|
|
2991
|
+
dangerouslyAllowBrowser: true,
|
|
2992
|
+
defaultHeaders,
|
|
2993
|
+
baseURL,
|
|
2994
|
+
fetch: fetchOverride ?? buildGuardedModelFetch(model),
|
|
2995
|
+
...buildOpenAISdkClientOptions(model)
|
|
2996
|
+
};
|
|
2997
|
+
if (isOpenAICompatibleAzureResponsesBaseUrl(baseURL)) return new OpenAI(clientOptions);
|
|
2998
|
+
return new AzureOpenAI({
|
|
2999
|
+
...clientOptions,
|
|
3000
|
+
apiVersion: resolveAzureOpenAIApiVersion()
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
//#endregion
|
|
3004
|
+
//#region packages/ai/src/transports/openai-completions-transport.ts
|
|
3005
|
+
function assertOpenAICompletionsPayloadHasConversationTurn(params, model) {
|
|
3006
|
+
const messages = params.messages;
|
|
3007
|
+
if (!Array.isArray(messages) || hasOpenAICompatibleConversationTurn(messages)) return;
|
|
3008
|
+
throw new Error(`OpenAI-compatible chat payload for ${model.provider}/${model.id} contains no non-empty user or assistant messages after compaction and transport transforms; refusing to send a system/tool-only request. Start a new user turn or repair the compacted session history.`);
|
|
3009
|
+
}
|
|
3010
|
+
const SSE_DONE_LINE_RE = /^data:[ \t]*\[DONE\][ \t]*$/i;
|
|
3011
|
+
const SSE_DONE_MAX_LINE_CHARS = 1024;
|
|
3012
|
+
function createSseDoneDetector() {
|
|
3013
|
+
const decoder = new TextDecoder();
|
|
3014
|
+
let line = "";
|
|
3015
|
+
let lineOverflowed = false;
|
|
3016
|
+
let sawDone = false;
|
|
3017
|
+
const finishLine = () => {
|
|
3018
|
+
if (!lineOverflowed && SSE_DONE_LINE_RE.test(line)) sawDone = true;
|
|
3019
|
+
line = "";
|
|
3020
|
+
lineOverflowed = false;
|
|
3021
|
+
};
|
|
3022
|
+
const observeText = (text) => {
|
|
3023
|
+
for (const char of text) {
|
|
3024
|
+
if (char === "\n" || char === "\r") {
|
|
3025
|
+
finishLine();
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
if (!lineOverflowed && line.length < SSE_DONE_MAX_LINE_CHARS) line += char;
|
|
3029
|
+
else lineOverflowed = true;
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
return {
|
|
3033
|
+
observe(chunk) {
|
|
3034
|
+
if (!sawDone) observeText(decoder.decode(chunk, { stream: true }));
|
|
3035
|
+
},
|
|
3036
|
+
finish() {
|
|
3037
|
+
if (sawDone) return;
|
|
3038
|
+
observeText(decoder.decode());
|
|
3039
|
+
if (line || lineOverflowed) finishLine();
|
|
3040
|
+
},
|
|
3041
|
+
sawDone: () => sawDone
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
function buildOpenAICompletionsClientConfig(model, headers) {
|
|
3045
|
+
const defaultQuery = {};
|
|
3046
|
+
let baseURL = model.baseUrl;
|
|
3047
|
+
let isAzureHost = false;
|
|
3048
|
+
const parsed = URL.parse(model.baseUrl);
|
|
3049
|
+
if (parsed) {
|
|
3050
|
+
isAzureHost = isAzureOpenAICompatibleHost(parsed.hostname.toLowerCase());
|
|
3051
|
+
parsed.searchParams.forEach((value, key) => {
|
|
3052
|
+
if (value) defaultQuery[key] = value;
|
|
3053
|
+
});
|
|
3054
|
+
parsed.search = "";
|
|
3055
|
+
baseURL = parsed.toString().replace(/\/$/, "");
|
|
3056
|
+
}
|
|
3057
|
+
if (isAzureHost) {
|
|
3058
|
+
const apiVersionHeader = Object.keys(headers).find((key) => key.toLowerCase() === "api-version");
|
|
3059
|
+
if (apiVersionHeader) {
|
|
3060
|
+
const apiVersion = headers[apiVersionHeader]?.trim();
|
|
3061
|
+
delete headers[apiVersionHeader];
|
|
3062
|
+
if (apiVersion && !defaultQuery["api-version"]) defaultQuery["api-version"] = apiVersion;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
return {
|
|
3066
|
+
baseURL: resolveOpenAIClientBaseUrl(model, baseURL),
|
|
3067
|
+
defaultHeaders: headers,
|
|
3068
|
+
defaultQuery: Object.keys(defaultQuery).length > 0 ? defaultQuery : void 0
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
function createDirectCompletionsEventStream(output, stream) {
|
|
3072
|
+
const finishedBlocks = /* @__PURE__ */ new Set();
|
|
3073
|
+
const contentIndices = /* @__PURE__ */ new WeakMap();
|
|
3074
|
+
let openTextBlock;
|
|
3075
|
+
let openThinkingBlock;
|
|
3076
|
+
const finishBlock = (block) => {
|
|
3077
|
+
const contentIndex = contentIndices.get(block);
|
|
3078
|
+
if (contentIndex === void 0 || finishedBlocks.has(block)) return;
|
|
3079
|
+
finishedBlocks.add(block);
|
|
3080
|
+
if (block.type === "text") {
|
|
3081
|
+
openTextBlock = void 0;
|
|
3082
|
+
stream.push({
|
|
3083
|
+
type: "text_end",
|
|
3084
|
+
contentIndex,
|
|
3085
|
+
content: block.text,
|
|
3086
|
+
partial: output
|
|
3087
|
+
});
|
|
3088
|
+
} else if (block.type === "thinking") {
|
|
3089
|
+
openThinkingBlock = void 0;
|
|
3090
|
+
stream.push({
|
|
3091
|
+
type: "thinking_end",
|
|
3092
|
+
contentIndex,
|
|
3093
|
+
content: block.thinking,
|
|
3094
|
+
partial: output
|
|
3095
|
+
});
|
|
3096
|
+
} else if (block.type === "toolCall") stream.push({
|
|
3097
|
+
type: "toolcall_end",
|
|
3098
|
+
contentIndex,
|
|
3099
|
+
toolCall: block,
|
|
3100
|
+
partial: output
|
|
3101
|
+
});
|
|
3102
|
+
};
|
|
3103
|
+
return {
|
|
3104
|
+
stream: { push(event) {
|
|
3105
|
+
if (event.type === "text_start" || event.type === "thinking_start" || event.type === "toolcall_start") {
|
|
3106
|
+
const block = output.content[event.contentIndex];
|
|
3107
|
+
if (block) {
|
|
3108
|
+
contentIndices.set(block, event.contentIndex);
|
|
3109
|
+
if (block.type === "text") openTextBlock = block;
|
|
3110
|
+
else if (block.type === "thinking") openThinkingBlock = block;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
stream.push(event);
|
|
3114
|
+
} },
|
|
3115
|
+
beforeContentBlock: (nextType) => {
|
|
3116
|
+
if (openThinkingBlock) finishBlock(openThinkingBlock);
|
|
3117
|
+
if (openTextBlock && nextType !== "toolCall") finishBlock(openTextBlock);
|
|
3118
|
+
},
|
|
3119
|
+
finish(includeToolCalls) {
|
|
3120
|
+
for (const block of output.content) if (block.type !== "toolCall" || includeToolCalls) finishBlock(block);
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
function createOpenAICompletionsTransportStreamFn() {
|
|
3125
|
+
const streamResponses = createOpenAIResponsesTransportStreamFn();
|
|
3126
|
+
return (model, context, options) => {
|
|
3127
|
+
const completionsOptions = options;
|
|
3128
|
+
if (model.reasoning && context.tools?.length && supportsModelTools(model) && isNativeOpenAIEndpoint(model)) {
|
|
3129
|
+
const toolChoice = completionsOptions?.toolChoice;
|
|
3130
|
+
const responsesOptions = {
|
|
3131
|
+
...options,
|
|
3132
|
+
...completionsOptions?.reasoning === void 0 && completionsOptions?.reasoningEffort === void 0 ? { reasoningEffort: "high" } : {},
|
|
3133
|
+
...toolChoice && typeof toolChoice === "object" ? { toolChoice: toOpenAIResponsesToolChoice(toolChoice) } : {}
|
|
3134
|
+
};
|
|
3135
|
+
return streamResponses(getAiTransportHost().inheritManagedTransport(model, {
|
|
3136
|
+
...model,
|
|
3137
|
+
api: "openai-responses"
|
|
3138
|
+
}), context, responsesOptions);
|
|
3139
|
+
}
|
|
3140
|
+
return streamOpenAICompletionsRequest(model, context, completionsOptions, "managed");
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
function streamOpenAICompletionsRequest(model, context, options, mode) {
|
|
3144
|
+
const { eventStream, stream } = createWritableTransportEventStream();
|
|
3145
|
+
(async () => {
|
|
3146
|
+
const output = createAssistantOutput(model);
|
|
3147
|
+
const provisionalCommentaryTags = /* @__PURE__ */ new Map();
|
|
3148
|
+
let firstEventAbort;
|
|
3149
|
+
try {
|
|
3150
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
3151
|
+
const directEmitReasoning = Boolean(mode === "direct" && model.reasoning && options?.reasoningEffort && isOpenAICompletionsThinkingEnabled(options.reasoningEffort));
|
|
3152
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
3153
|
+
const policy = mode === "direct" ? {
|
|
3154
|
+
mode,
|
|
3155
|
+
compat: resolveOpenAICompletionsCompat(model),
|
|
3156
|
+
cacheRetention
|
|
3157
|
+
} : { mode };
|
|
3158
|
+
const { client, sawStreamDONE } = policy.mode === "direct" ? {
|
|
3159
|
+
client: createDirectCompletionsClient(model, context, apiKey, resolveProviderSimpleCompletionHeaders(model, options), cacheRetention === "none" ? void 0 : options?.sessionId, policy.compat),
|
|
3160
|
+
sawStreamDONE: void 0
|
|
3161
|
+
} : createManagedCompletionsClient(model, context, options, apiKey, cacheRetention);
|
|
3162
|
+
let params = buildOpenAICompletionsRequest(model, context, options, policy);
|
|
3163
|
+
const encodeBody = prepareModelRequestBody(options);
|
|
3164
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
3165
|
+
if (nextParams !== void 0) params = nextParams;
|
|
3166
|
+
if (mode === "managed") {
|
|
3167
|
+
if (options?.openclawCodeModeToolSurface === true) {
|
|
3168
|
+
const visibleToolNames = resolveCodeModeResponsesVisibleToolNames(context);
|
|
3169
|
+
enforceCodeModeResponsesToolSurface(params, visibleToolNames, void 0, codeModeToolSurfaceObserver.get(options));
|
|
3170
|
+
assertCodeModeResponsesToolSurface(params, visibleToolNames);
|
|
3171
|
+
}
|
|
3172
|
+
if (getCompat(model).requiresNonEmptyUserOrAssistantMessage) assertOpenAICompletionsPayloadHasConversationTurn(params, model);
|
|
3173
|
+
}
|
|
3174
|
+
const emitReasoning = mode === "direct" ? directEmitReasoning : shouldEmitOpenAICompletionsReasoning(model, options);
|
|
3175
|
+
firstEventAbort = createFirstStreamEventAbortController(options?.signal);
|
|
3176
|
+
const requestOptions = mode === "direct" ? {
|
|
3177
|
+
...await encodeBody(params),
|
|
3178
|
+
signal: firstEventAbort.signal,
|
|
3179
|
+
...options?.timeoutMs !== void 0 ? { timeout: options.timeoutMs } : {},
|
|
3180
|
+
maxRetries: 0
|
|
3181
|
+
} : {
|
|
3182
|
+
...buildOpenAISdkRequestOptions(model, firstEventAbort.signal, { timeoutMs: options?.timeoutMs }),
|
|
3183
|
+
...await encodeBody(params)
|
|
3184
|
+
};
|
|
3185
|
+
const request = client.chat.completions.create(params, requestOptions);
|
|
3186
|
+
const response = await request.asResponse();
|
|
3187
|
+
const hookedResponseStream = withProviderResponseHook({
|
|
3188
|
+
stream: { async *[Symbol.asyncIterator]() {
|
|
3189
|
+
if (!params.stream) {
|
|
3190
|
+
yield await boundResponseBody(response, {
|
|
3191
|
+
maxBytes: 16777216,
|
|
3192
|
+
onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`Chat Completions JSON response exceeds ${maxBytes} bytes`)
|
|
3193
|
+
}).json();
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
const data = await request;
|
|
3197
|
+
if (Symbol.asyncIterator in data) yield* data;
|
|
3198
|
+
else yield data;
|
|
3199
|
+
} },
|
|
3200
|
+
signal: firstEventAbort.signal,
|
|
3201
|
+
abort: firstEventAbort.abort,
|
|
3202
|
+
hook: createOpenAIProviderAcceptanceHook(options, response, model),
|
|
3203
|
+
onReady: () => stream.push({
|
|
3204
|
+
type: "start",
|
|
3205
|
+
partial: output
|
|
3206
|
+
})
|
|
3207
|
+
});
|
|
3208
|
+
const directEvents = mode === "direct" ? createDirectCompletionsEventStream(output, stream) : void 0;
|
|
3209
|
+
try {
|
|
3210
|
+
await processCompletionsStream(hookedResponseStream, output, model, directEvents?.stream ?? stream, {
|
|
3211
|
+
...directEvents ? {
|
|
3212
|
+
mode: "direct",
|
|
3213
|
+
beforeContentBlock: directEvents.beforeContentBlock,
|
|
3214
|
+
provisionalCommentaryTags
|
|
3215
|
+
} : { mode: "managed" },
|
|
3216
|
+
signal: options?.signal,
|
|
3217
|
+
emitReasoning,
|
|
3218
|
+
strictReasoningTags: reasoningTagTextPolicy.isStrict(options),
|
|
3219
|
+
firstEventTimeoutMs: getFirstStreamEventTimeoutMs(options),
|
|
3220
|
+
abortFirstEventStream: firstEventAbort.abort,
|
|
3221
|
+
onFirstEventTimeout: getFirstStreamEventTimeoutHandler(options),
|
|
3222
|
+
sawStreamDONE
|
|
3223
|
+
});
|
|
3224
|
+
if (directEvents) {
|
|
3225
|
+
if (options?.signal?.aborted) throw transportAbortError(options.signal);
|
|
3226
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") throw new Error(output.errorMessage || (output.stopReason === "aborted" ? "Request was aborted" : "Provider returned an invalid tool call"));
|
|
3227
|
+
}
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
directEvents?.finish(false);
|
|
3230
|
+
throw error;
|
|
3231
|
+
}
|
|
3232
|
+
directEvents?.finish(output.stopReason === "toolUse");
|
|
3233
|
+
finalizeTransportStream({
|
|
3234
|
+
stream,
|
|
3235
|
+
output,
|
|
3236
|
+
signal: options?.signal
|
|
3237
|
+
});
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
failTransportStream({
|
|
3240
|
+
stream,
|
|
3241
|
+
output,
|
|
3242
|
+
signal: options?.signal,
|
|
3243
|
+
error,
|
|
3244
|
+
cleanup: () => {
|
|
3245
|
+
finalizeOpenAICompletionsToolCalls(output, { allowSilentToolCallPromotion: false });
|
|
3246
|
+
clearPendingCommentaryText(provisionalCommentaryTags);
|
|
3247
|
+
tagUnresolvedTextAsCommentary(output);
|
|
3248
|
+
if (mode === "direct") for (const block of output.content) {
|
|
3249
|
+
delete block.index;
|
|
3250
|
+
delete block.partialArgs;
|
|
3251
|
+
delete block.streamIndex;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
} finally {
|
|
3256
|
+
firstEventAbort?.dispose();
|
|
3257
|
+
}
|
|
3258
|
+
})();
|
|
3259
|
+
return eventStream;
|
|
3260
|
+
}
|
|
3261
|
+
function createDirectCompletionsClient(model, context, apiKey, optionsHeaders, sessionId, compat) {
|
|
3262
|
+
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
|
|
3263
|
+
const headers = { ...model.headers };
|
|
3264
|
+
if (model.provider === "github-copilot") Object.assign(headers, buildCopilotDynamicHeaders(context.messages));
|
|
3265
|
+
if (sessionId && compat.sessionAffinity !== "none") {
|
|
3266
|
+
if (compat.sessionAffinity === "openrouter") headers["x-session-id"] = sessionId;
|
|
3267
|
+
else {
|
|
3268
|
+
headers.session_id = sessionId;
|
|
3269
|
+
headers["x-client-request-id"] = sessionId;
|
|
3270
|
+
headers["x-session-affinity"] = sessionId;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return createOpenAIProviderClient(model, apiKey, headers, optionsHeaders);
|
|
3274
|
+
}
|
|
3275
|
+
function createManagedCompletionsClient(model, context, options, apiKey, cacheRetention) {
|
|
3276
|
+
const turnState = resolveProviderTransportTurnState(model, {
|
|
3277
|
+
sessionId: options?.sessionId,
|
|
3278
|
+
turnId: randomUUID(),
|
|
3279
|
+
attempt: 1,
|
|
3280
|
+
transport: "stream"
|
|
3281
|
+
});
|
|
3282
|
+
const optionHeaders = resolveOpencodeSessionHeaders(model, options);
|
|
3283
|
+
const turnHeaders = filterProviderTurnHeadersForExplicitOpencodeSession(model, options, turnState?.headers);
|
|
3284
|
+
const doneDetector = createSseDoneDetector();
|
|
3285
|
+
const baseFetch = buildGuardedModelFetch(model, void 0, { onSseComment: () => notifyLlmRequestActivity(options?.signal, false) });
|
|
3286
|
+
const doneDetectingFetch = async (url, init) => {
|
|
3287
|
+
const response = await baseFetch(url, init);
|
|
3288
|
+
if (!response.body || !response.ok) return response;
|
|
3289
|
+
if (typeof TransformStream === "undefined" || !response.body.pipeThrough) return response;
|
|
3290
|
+
const transformed = response.body.pipeThrough(new TransformStream({
|
|
3291
|
+
transform(chunk, controller) {
|
|
3292
|
+
doneDetector.observe(chunk);
|
|
3293
|
+
controller.enqueue(chunk);
|
|
3294
|
+
},
|
|
3295
|
+
flush() {
|
|
3296
|
+
doneDetector.finish();
|
|
3297
|
+
}
|
|
3298
|
+
}));
|
|
3299
|
+
return new Response(transformed, response);
|
|
3300
|
+
};
|
|
3301
|
+
const clientConfig = buildOpenAICompletionsClientConfig(model, buildOpenAIClientHeaders(model, context, {
|
|
3302
|
+
...turnHeaders,
|
|
3303
|
+
...optionHeaders
|
|
3304
|
+
}, void 0, resolvePromptCacheKey(options, cacheRetention), cacheRetention));
|
|
3305
|
+
return {
|
|
3306
|
+
client: new OpenAI({
|
|
3307
|
+
apiKey,
|
|
3308
|
+
baseURL: clientConfig.baseURL,
|
|
3309
|
+
dangerouslyAllowBrowser: true,
|
|
3310
|
+
defaultHeaders: clientConfig.defaultHeaders,
|
|
3311
|
+
defaultQuery: clientConfig.defaultQuery,
|
|
3312
|
+
fetch: doneDetectingFetch,
|
|
3313
|
+
...buildOpenAISdkClientOptions(model)
|
|
3314
|
+
}),
|
|
3315
|
+
sawStreamDONE: doneDetector.sawDone
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
//#endregion
|
|
3319
|
+
export { requestPreparedOpenAIResponsesCompaction as a, convertMessages as c, applyCompletionsAnthropicCacheControl as d, hasOpenAICompatibleConversationTurn as f, createDeepSeekTextFilter as h, createOpenAIResponsesTransportStreamFn as i, flattenCompletionMessagesToStringContent as l, resolveMaxTokensParam as m, streamOpenAICompletionsRequest as n, hasResponsesWebSearchTool as o, canonicalizeMaxTokensParam as p, createAzureOpenAIResponsesTransportStreamFn as r, buildOpenAICompletionsParams as s, createOpenAICompletionsTransportStreamFn as t, stripCompletionMessagesToRoleContent as u };
|