@jecruz/merc-ai 0.1.3 → 0.1.6

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.
Files changed (111) hide show
  1. package/README.md +36 -9
  2. package/dist/env-api-keys.d.ts.map +1 -1
  3. package/dist/env-api-keys.js +1 -1
  4. package/dist/env-api-keys.js.map +1 -1
  5. package/dist/image-models.generated.d.ts +345 -0
  6. package/dist/image-models.generated.d.ts.map +1 -1
  7. package/dist/image-models.generated.js +348 -3
  8. package/dist/image-models.generated.js.map +1 -1
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/models.d.ts.map +1 -1
  14. package/dist/models.generated.d.ts +15282 -3882
  15. package/dist/models.generated.d.ts.map +1 -1
  16. package/dist/models.generated.js +18368 -10964
  17. package/dist/models.generated.js.map +1 -1
  18. package/dist/models.js +4 -1
  19. package/dist/models.js.map +1 -1
  20. package/dist/providers/amazon-bedrock.d.ts.map +1 -1
  21. package/dist/providers/amazon-bedrock.js +188 -19
  22. package/dist/providers/amazon-bedrock.js.map +1 -1
  23. package/dist/providers/anthropic.d.ts.map +1 -1
  24. package/dist/providers/anthropic.js +101 -34
  25. package/dist/providers/anthropic.js.map +1 -1
  26. package/dist/providers/azure-openai-responses.d.ts +2 -0
  27. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  28. package/dist/providers/azure-openai-responses.js +8 -3
  29. package/dist/providers/azure-openai-responses.js.map +1 -1
  30. package/dist/providers/google-shared.d.ts +15 -4
  31. package/dist/providers/google-shared.d.ts.map +1 -1
  32. package/dist/providers/google-shared.js +80 -15
  33. package/dist/providers/google-shared.js.map +1 -1
  34. package/dist/providers/google-vertex.d.ts.map +1 -1
  35. package/dist/providers/google-vertex.js +23 -60
  36. package/dist/providers/google-vertex.js.map +1 -1
  37. package/dist/providers/google.d.ts.map +1 -1
  38. package/dist/providers/google.js +19 -68
  39. package/dist/providers/google.js.map +1 -1
  40. package/dist/providers/mistral.d.ts +1 -1
  41. package/dist/providers/mistral.d.ts.map +1 -1
  42. package/dist/providers/mistral.js +44 -60
  43. package/dist/providers/mistral.js.map +1 -1
  44. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  45. package/dist/providers/openai-codex-responses.js +250 -124
  46. package/dist/providers/openai-codex-responses.js.map +1 -1
  47. package/dist/providers/openai-completions.d.ts.map +1 -1
  48. package/dist/providers/openai-completions.js +88 -38
  49. package/dist/providers/openai-completions.js.map +1 -1
  50. package/dist/providers/openai-prompt-cache.d.ts +3 -0
  51. package/dist/providers/openai-prompt-cache.d.ts.map +1 -0
  52. package/dist/providers/openai-prompt-cache.js +10 -0
  53. package/dist/providers/openai-prompt-cache.js.map +1 -0
  54. package/dist/providers/openai-responses-shared.d.ts.map +1 -1
  55. package/dist/providers/openai-responses-shared.js +154 -211
  56. package/dist/providers/openai-responses-shared.js.map +1 -1
  57. package/dist/providers/openai-responses.d.ts.map +1 -1
  58. package/dist/providers/openai-responses.js +15 -9
  59. package/dist/providers/openai-responses.js.map +1 -1
  60. package/dist/providers/openrouter-images.d.ts.map +1 -1
  61. package/dist/providers/openrouter-images.js +5 -2
  62. package/dist/providers/openrouter-images.js.map +1 -1
  63. package/dist/providers/simple-options.d.ts +15 -3
  64. package/dist/providers/simple-options.d.ts.map +1 -1
  65. package/dist/providers/simple-options.js +34 -4
  66. package/dist/providers/simple-options.js.map +1 -1
  67. package/dist/types.d.ts +35 -2
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js.map +1 -1
  70. package/dist/utils/error-body.d.ts +5 -0
  71. package/dist/utils/error-body.d.ts.map +1 -1
  72. package/dist/utils/error-body.js +53 -5
  73. package/dist/utils/error-body.js.map +1 -1
  74. package/dist/utils/estimate.d.ts +22 -0
  75. package/dist/utils/estimate.d.ts.map +1 -0
  76. package/dist/utils/estimate.js +104 -0
  77. package/dist/utils/estimate.js.map +1 -0
  78. package/dist/utils/event-stream.d.ts.map +1 -1
  79. package/dist/utils/event-stream.js +25 -7
  80. package/dist/utils/event-stream.js.map +1 -1
  81. package/dist/utils/json-parse.d.ts +3 -0
  82. package/dist/utils/json-parse.d.ts.map +1 -1
  83. package/dist/utils/json-parse.js +15 -0
  84. package/dist/utils/json-parse.js.map +1 -1
  85. package/dist/utils/oauth/anthropic.d.ts.map +1 -1
  86. package/dist/utils/oauth/anthropic.js +16 -10
  87. package/dist/utils/oauth/anthropic.js.map +1 -1
  88. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  89. package/dist/utils/oauth/github-copilot.js +57 -21
  90. package/dist/utils/oauth/github-copilot.js.map +1 -1
  91. package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  92. package/dist/utils/oauth/openai-codex.js +73 -16
  93. package/dist/utils/oauth/openai-codex.js.map +1 -1
  94. package/dist/utils/oauth/types.d.ts +2 -0
  95. package/dist/utils/oauth/types.d.ts.map +1 -1
  96. package/dist/utils/oauth/types.js.map +1 -1
  97. package/dist/utils/overflow.d.ts +9 -1
  98. package/dist/utils/overflow.d.ts.map +1 -1
  99. package/dist/utils/overflow.js +23 -6
  100. package/dist/utils/overflow.js.map +1 -1
  101. package/dist/utils/provider-env.d.ts.map +1 -1
  102. package/dist/utils/provider-env.js +45 -10
  103. package/dist/utils/provider-env.js.map +1 -1
  104. package/dist/utils/tool-call-id.d.ts +11 -0
  105. package/dist/utils/tool-call-id.d.ts.map +1 -0
  106. package/dist/utils/tool-call-id.js +96 -0
  107. package/dist/utils/tool-call-id.js.map +1 -0
  108. package/dist/utils/validation.d.ts.map +1 -1
  109. package/dist/utils/validation.js +9 -0
  110. package/dist/utils/validation.js.map +1 -1
  111. package/package.json +5 -2
@@ -1 +1 @@
1
- {"version":3,"file":"openai-responses-shared.d.ts","sourceRoot":"","sources":["../../src/providers/openai-responses-shared.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,IAAI,IAAI,UAAU,EAClB,6BAA6B,EAG7B,aAAa,EAOb,mBAAmB,EAEnB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAChB,OAAO,EAEP,KAAK,EAKL,IAAI,EAEJ,KAAK,EACL,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAoC5E,MAAM,WAAW,4BAA4B;IAC5C,WAAW,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC;IAC5D,kBAAkB,CAAC,EAAE,CACpB,mBAAmB,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,EAC9E,kBAAkB,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,KACzE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAC/D,uBAAuB,CAAC,EAAE,CACzB,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,KAClE,IAAI,CAAC;CACV;AAED,MAAM,WAAW,+BAA+B;IAC/C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,4BAA4B;IAC5C,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,wBAAwB,CAAC,IAAI,SAAS,GAAG,EACxD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAAC,EAC7C,OAAO,CAAC,EAAE,+BAA+B,GACvC,aAAa,CAmMf;AAMD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,4BAA4B,GAAG,UAAU,EAAE,CAUzG;AAMD,wBAAsB,sBAAsB,CAAC,IAAI,SAAS,GAAG,EAC5D,YAAY,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAChD,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,2BAA2B,EACnC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,CAAC,EAAE,4BAA4B,GACpC,OAAO,CAAC,IAAI,CAAC,CAwQf","sourcesContent":["import type OpenAI from \"openai\";\nimport type {\n\tTool as OpenAITool,\n\tResponseCreateParamsStreaming,\n\tResponseFunctionCallOutputItemList,\n\tResponseFunctionToolCall,\n\tResponseInput,\n\tResponseInputContent,\n\tResponseInputImage,\n\tResponseInputItem,\n\tResponseInputText,\n\tResponseOutputMessage,\n\tResponseReasoningItem,\n\tResponseStreamEvent,\n\tResponseToolSearchOutputItemParam,\n} from \"openai/resources/responses/responses.js\";\nimport { calculateCost } from \"../models.js\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tImageContent,\n\tModel,\n\tStopReason,\n\tTextContent,\n\tTextSignatureV1,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tUsage,\n} from \"../types.js\";\nimport type { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport { shortHash } from \"../utils/hash.js\";\nimport { parseStreamingJson } from \"../utils/json-parse.js\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.js\";\nimport { transformMessages } from \"./transform-messages.js\";\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\nfunction encodeTextSignatureV1(id: string, phase?: TextSignatureV1[\"phase\"]): string {\n\tconst payload: TextSignatureV1 = { v: 1, id };\n\tif (phase) payload.phase = phase;\n\treturn JSON.stringify(payload);\n}\n\nfunction parseTextSignature(\n\tsignature: string | undefined,\n): { id: string; phase?: TextSignatureV1[\"phase\"] } | undefined {\n\tif (!signature) return undefined;\n\tif (signature.startsWith(\"{\")) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(signature) as Partial<TextSignatureV1>;\n\t\t\tif (parsed.v === 1 && typeof parsed.id === \"string\") {\n\t\t\t\tif (parsed.phase === \"commentary\" || parsed.phase === \"final_answer\") {\n\t\t\t\t\treturn { id: parsed.id, phase: parsed.phase };\n\t\t\t\t}\n\t\t\t\treturn { id: parsed.id };\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to legacy plain-string handling.\n\t\t}\n\t}\n\treturn { id: signature };\n}\n\nexport interface OpenAIResponsesStreamOptions {\n\tserviceTier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\tresolveServiceTier?: (\n\t\tresponseServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t\trequestServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t) => ResponseCreateParamsStreaming[\"service_tier\"] | undefined;\n\tapplyServiceTierPricing?: (\n\t\tusage: Usage,\n\t\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t) => void;\n}\n\nexport interface ConvertResponsesMessagesOptions {\n\tincludeSystemPrompt?: boolean;\n\tdeferredTools?: ReadonlyMap<string, Tool>;\n}\n\nexport interface ConvertResponsesToolsOptions {\n\tstrict?: boolean | null;\n\tdeferLoading?: boolean;\n}\n\n// =============================================================================\n// Message conversion\n// =============================================================================\n\nexport function convertResponsesMessages<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\tallowedToolCallProviders: ReadonlySet<string>,\n\toptions?: ConvertResponsesMessagesOptions,\n): ResponseInput {\n\tconst messages: ResponseInput = [];\n\tconst loadedToolNames = new Set<string>();\n\n\tconst normalizeIdPart = (part: string): string => {\n\t\tconst sanitized = part.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n\t\tconst normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;\n\t\treturn normalized.replace(/_+$/, \"\");\n\t};\n\n\tconst buildForeignResponsesItemId = (itemId: string): string => {\n\t\tconst normalized = `fc_${shortHash(itemId)}`;\n\t\treturn normalized.length > 64 ? normalized.slice(0, 64) : normalized;\n\t};\n\n\tconst normalizeToolCallId = (id: string, _targetModel: Model<TApi>, source: AssistantMessage): string => {\n\t\tif (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id);\n\t\tif (!id.includes(\"|\")) return normalizeIdPart(id);\n\t\tconst [callId, itemId] = id.split(\"|\");\n\t\tconst normalizedCallId = normalizeIdPart(callId);\n\t\tconst isForeignToolCall = source.provider !== model.provider || source.api !== model.api;\n\t\tlet normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);\n\t\t// OpenAI Responses API requires item id to start with \"fc\"\n\t\tif (!normalizedItemId.startsWith(\"fc_\")) {\n\t\t\tnormalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);\n\t\t}\n\t\treturn `${normalizedCallId}|${normalizedItemId}`;\n\t};\n\n\tconst transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);\n\n\tconst includeSystemPrompt = options?.includeSystemPrompt ?? true;\n\tif (includeSystemPrompt && context.systemPrompt) {\n\t\tconst compat = model.compat as { supportsDeveloperRole?: boolean } | undefined;\n\t\tconst role = model.reasoning && compat?.supportsDeveloperRole !== false ? \"developer\" : \"system\";\n\t\tmessages.push({\n\t\t\trole,\n\t\t\tcontent: sanitizeSurrogates(context.systemPrompt),\n\t\t});\n\t}\n\n\tlet msgIndex = 0;\n\tfor (const msg of transformedMessages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tmessages.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"input_text\", text: sanitizeSurrogates(msg.content) }],\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"input_text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t} satisfies ResponseInputText;\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"input_image\",\n\t\t\t\t\t\tdetail: \"auto\",\n\t\t\t\t\t\timage_url: `data:${item.mimeType};base64,${item.data}`,\n\t\t\t\t\t} satisfies ResponseInputImage;\n\t\t\t\t});\n\t\t\t\tif (content.length === 0) continue;\n\t\t\t\tmessages.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst output: ResponseInput = [];\n\t\t\tconst assistantMsg = msg as AssistantMessage;\n\t\t\tconst isDifferentModel =\n\t\t\t\tassistantMsg.model !== model.id &&\n\t\t\t\tassistantMsg.provider === model.provider &&\n\t\t\t\tassistantMsg.api === model.api;\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"thinking\") {\n\t\t\t\t\tif (block.thinkingSignature) {\n\t\t\t\t\t\tconst reasoningItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;\n\t\t\t\t\t\toutput.push(reasoningItem);\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"text\") {\n\t\t\t\t\tconst textBlock = block as TextContent;\n\t\t\t\t\tconst parsedSignature = parseTextSignature(textBlock.textSignature);\n\t\t\t\t\t// OpenAI requires id to be max 64 characters\n\t\t\t\t\tlet msgId = parsedSignature?.id;\n\t\t\t\t\tif (!msgId) {\n\t\t\t\t\t\tmsgId = `msg_${msgIndex}`;\n\t\t\t\t\t} else if (msgId.length > 64) {\n\t\t\t\t\t\tmsgId = `msg_${shortHash(msgId)}`;\n\t\t\t\t\t}\n\t\t\t\t\toutput.push({\n\t\t\t\t\t\ttype: \"message\",\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tcontent: [{ type: \"output_text\", text: sanitizeSurrogates(textBlock.text), annotations: [] }],\n\t\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\t\tid: msgId,\n\t\t\t\t\t\tphase: parsedSignature?.phase,\n\t\t\t\t\t} satisfies ResponseOutputMessage);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst toolCall = block as ToolCall;\n\t\t\t\t\tconst [callId, itemIdRaw] = toolCall.id.split(\"|\");\n\t\t\t\t\tlet itemId: string | undefined = itemIdRaw;\n\n\t\t\t\t\t// For different-model messages, set id to undefined to avoid pairing validation.\n\t\t\t\t\t// OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items.\n\t\t\t\t\t// By omitting the id, we avoid triggering that validation (like cross-provider does).\n\t\t\t\t\tif (isDifferentModel && itemId?.startsWith(\"fc_\")) {\n\t\t\t\t\t\titemId = undefined;\n\t\t\t\t\t}\n\n\t\t\t\t\toutput.push({\n\t\t\t\t\t\ttype: \"function_call\",\n\t\t\t\t\t\tid: itemId,\n\t\t\t\t\t\tcall_id: callId,\n\t\t\t\t\t\tname: toolCall.name,\n\t\t\t\t\t\targuments: JSON.stringify(toolCall.arguments),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (output.length === 0) continue;\n\t\t\tmessages.push(...output);\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst textResult = msg.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst hasImages = msg.content.some((c): c is ImageContent => c.type === \"image\");\n\t\t\tconst hasText = textResult.length > 0;\n\t\t\tconst [callId] = msg.toolCallId.split(\"|\");\n\n\t\t\tlet output: string | ResponseFunctionCallOutputItemList;\n\t\t\tif (hasImages && model.input.includes(\"image\")) {\n\t\t\t\tconst contentParts: ResponseFunctionCallOutputItemList = [];\n\n\t\t\t\tif (hasText) {\n\t\t\t\t\tcontentParts.push({\n\t\t\t\t\t\ttype: \"input_text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(textResult),\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const block of msg.content) {\n\t\t\t\t\tif (block.type === \"image\") {\n\t\t\t\t\t\tcontentParts.push({\n\t\t\t\t\t\t\ttype: \"input_image\",\n\t\t\t\t\t\t\tdetail: \"auto\",\n\t\t\t\t\t\t\timage_url: `data:${block.mimeType};base64,${block.data}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\toutput = contentParts;\n\t\t\t} else {\n\t\t\t\toutput = sanitizeSurrogates(hasText ? textResult : hasImages ? \"(see attached image)\" : \"(no tool output)\");\n\t\t\t}\n\n\t\t\tmessages.push({\n\t\t\t\ttype: \"function_call_output\",\n\t\t\t\tcall_id: callId,\n\t\t\t\toutput,\n\t\t\t});\n\n\t\t\tconst deferredTools: Tool[] = [];\n\t\t\tfor (const name of msg.addedToolNames ?? []) {\n\t\t\t\tconst tool = options?.deferredTools?.get(name);\n\t\t\t\tif (!tool || loadedToolNames.has(name)) continue;\n\t\t\t\tloadedToolNames.add(name);\n\t\t\t\tdeferredTools.push(tool);\n\t\t\t}\n\t\t\tif (deferredTools.length > 0) {\n\t\t\t\tconst names = deferredTools.map((tool) => tool.name);\n\t\t\t\tconst searchCallId = `pi_tool_load_${shortHash(`${msg.toolCallId}:${names.join(\",\")}`)}`;\n\t\t\t\tmessages.push({\n\t\t\t\t\ttype: \"tool_search_call\",\n\t\t\t\t\tcall_id: searchCallId,\n\t\t\t\t\texecution: \"client\",\n\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\targuments: { query: names.join(\" \"), limit: names.length },\n\t\t\t\t} satisfies ResponseInputItem);\n\t\t\t\tmessages.push({\n\t\t\t\t\ttype: \"tool_search_output\",\n\t\t\t\t\tcall_id: searchCallId,\n\t\t\t\t\texecution: \"client\",\n\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\ttools: convertResponsesTools(deferredTools, { deferLoading: true }),\n\t\t\t\t} satisfies ResponseToolSearchOutputItemParam);\n\t\t\t}\n\t\t}\n\t\tmsgIndex++;\n\t}\n\n\treturn messages;\n}\n\n// =============================================================================\n// Tool conversion\n// =============================================================================\n\nexport function convertResponsesTools(tools: Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] {\n\tconst strict = options?.strict === undefined ? false : options.strict;\n\treturn tools.map((tool) => ({\n\t\ttype: \"function\",\n\t\tname: tool.name,\n\t\tdescription: tool.description,\n\t\tparameters: tool.parameters as any, // TypeBox already generates JSON Schema\n\t\tstrict,\n\t\t...(options?.deferLoading ? { defer_loading: true } : {}),\n\t}));\n}\n\n// =============================================================================\n// Stream processing\n// =============================================================================\n\nexport async function processResponsesStream<TApi extends Api>(\n\topenaiStream: AsyncIterable<ResponseStreamEvent>,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<TApi>,\n\toptions?: OpenAIResponsesStreamOptions,\n): Promise<void> {\n\tlet currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null;\n\tlet currentBlock: ThinkingContent | TextContent | (ToolCall & { partialJson: string }) | null = null;\n\tlet terminalEventSeen = false;\n\tconst reasoningBlocksById = new Map<string, ThinkingContent>();\n\tconst blocks = output.content;\n\tconst blockIndex = () => blocks.length - 1;\n\tconst backfillReasoningSignatures = (responseOutput: ResponseReasoningItem[]): void => {\n\t\tfor (const item of responseOutput) {\n\t\t\tif (!item.encrypted_content) continue;\n\t\t\tconst block = reasoningBlocksById.get(item.id);\n\t\t\tif (!block?.thinkingSignature) continue;\n\n\t\t\tconst storedItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;\n\t\t\tif (storedItem.encrypted_content) continue;\n\t\t\tblock.thinkingSignature = JSON.stringify({ ...storedItem, encrypted_content: item.encrypted_content });\n\t\t}\n\t};\n\n\tfor await (const event of openaiStream) {\n\t\tif (event.type === \"response.created\") {\n\t\t\toutput.responseId = event.response.id;\n\t\t} else if (event.type === \"response.output_item.added\") {\n\t\t\tconst item = event.item;\n\t\t\tif (item.type === \"reasoning\") {\n\t\t\t\tcurrentItem = item;\n\t\t\t\tcurrentBlock = { type: \"thinking\", thinking: \"\" };\n\t\t\t\toutput.content.push(currentBlock);\n\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: blockIndex(), partial: output });\n\t\t\t} else if (item.type === \"message\") {\n\t\t\t\tcurrentItem = item;\n\t\t\t\tcurrentBlock = { type: \"text\", text: \"\" };\n\t\t\t\toutput.content.push(currentBlock);\n\t\t\t\tstream.push({ type: \"text_start\", contentIndex: blockIndex(), partial: output });\n\t\t\t} else if (item.type === \"function_call\") {\n\t\t\t\tcurrentItem = item;\n\t\t\t\tcurrentBlock = {\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\tid: `${item.call_id}|${item.id}`,\n\t\t\t\t\tname: item.name,\n\t\t\t\t\targuments: {},\n\t\t\t\t\tpartialJson: item.arguments || \"\",\n\t\t\t\t};\n\t\t\t\toutput.content.push(currentBlock);\n\t\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: blockIndex(), partial: output });\n\t\t\t}\n\t\t} else if (event.type === \"response.reasoning_summary_part.added\") {\n\t\t\tif (currentItem && currentItem.type === \"reasoning\") {\n\t\t\t\tcurrentItem.summary = currentItem.summary || [];\n\t\t\t\tcurrentItem.summary.push(event.part);\n\t\t\t}\n\t\t} else if (event.type === \"response.reasoning_summary_text.delta\") {\n\t\t\tif (currentItem?.type === \"reasoning\" && currentBlock?.type === \"thinking\") {\n\t\t\t\tcurrentItem.summary = currentItem.summary || [];\n\t\t\t\tconst lastPart = currentItem.summary[currentItem.summary.length - 1];\n\t\t\t\tif (lastPart) {\n\t\t\t\t\tcurrentBlock.thinking += event.delta;\n\t\t\t\t\tlastPart.text += event.delta;\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\t\tdelta: event.delta,\n\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.reasoning_summary_part.done\") {\n\t\t\tif (currentItem?.type === \"reasoning\" && currentBlock?.type === \"thinking\") {\n\t\t\t\tcurrentItem.summary = currentItem.summary || [];\n\t\t\t\tconst lastPart = currentItem.summary[currentItem.summary.length - 1];\n\t\t\t\tif (lastPart) {\n\t\t\t\t\tcurrentBlock.thinking += \"\\n\\n\";\n\t\t\t\t\tlastPart.text += \"\\n\\n\";\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\t\tdelta: \"\\n\\n\",\n\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.reasoning_text.delta\") {\n\t\t\tif (currentItem?.type === \"reasoning\" && currentBlock?.type === \"thinking\") {\n\t\t\t\tcurrentBlock.thinking += event.delta;\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\tdelta: event.delta,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (event.type === \"response.content_part.added\") {\n\t\t\tif (currentItem?.type === \"message\") {\n\t\t\t\tcurrentItem.content = currentItem.content || [];\n\t\t\t\t// Filter out ReasoningText, only accept output_text and refusal\n\t\t\t\tif (event.part.type === \"output_text\" || event.part.type === \"refusal\") {\n\t\t\t\t\tcurrentItem.content.push(event.part);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.output_text.delta\") {\n\t\t\tif (currentItem?.type === \"message\" && currentBlock?.type === \"text\") {\n\t\t\t\tif (!currentItem.content || currentItem.content.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst lastPart = currentItem.content[currentItem.content.length - 1];\n\t\t\t\tif (lastPart?.type === \"output_text\") {\n\t\t\t\t\tcurrentBlock.text += event.delta;\n\t\t\t\t\tlastPart.text += event.delta;\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\t\tdelta: event.delta,\n\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.refusal.delta\") {\n\t\t\tif (currentItem?.type === \"message\" && currentBlock?.type === \"text\") {\n\t\t\t\tif (!currentItem.content || currentItem.content.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst lastPart = currentItem.content[currentItem.content.length - 1];\n\t\t\t\tif (lastPart?.type === \"refusal\") {\n\t\t\t\t\tcurrentBlock.text += event.delta;\n\t\t\t\t\tlastPart.refusal += event.delta;\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\t\tdelta: event.delta,\n\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.function_call_arguments.delta\") {\n\t\t\tif (currentItem?.type === \"function_call\" && currentBlock?.type === \"toolCall\") {\n\t\t\t\tcurrentBlock.partialJson += event.delta;\n\t\t\t\tcurrentBlock.arguments = parseStreamingJson(currentBlock.partialJson);\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\tdelta: event.delta,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (event.type === \"response.function_call_arguments.done\") {\n\t\t\tif (currentItem?.type === \"function_call\" && currentBlock?.type === \"toolCall\") {\n\t\t\t\tconst previousPartialJson = currentBlock.partialJson;\n\t\t\t\tcurrentBlock.partialJson = event.arguments;\n\t\t\t\tcurrentBlock.arguments = parseStreamingJson(currentBlock.partialJson);\n\n\t\t\t\tif (event.arguments.startsWith(previousPartialJson)) {\n\t\t\t\t\tconst delta = event.arguments.slice(previousPartialJson.length);\n\t\t\t\t\tif (delta.length > 0) {\n\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\t\t\tdelta,\n\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.output_item.done\") {\n\t\t\tconst item = event.item;\n\n\t\t\tif (item.type === \"reasoning\" && currentBlock?.type === \"thinking\") {\n\t\t\t\tconst summaryText = item.summary?.map((s) => s.text).join(\"\\n\\n\") || \"\";\n\t\t\t\tconst contentText = item.content?.map((c) => c.text).join(\"\\n\\n\") || \"\";\n\t\t\t\tcurrentBlock.thinking = summaryText || contentText || currentBlock.thinking;\n\t\t\t\tcurrentBlock.thinkingSignature = JSON.stringify(item);\n\t\t\t\treasoningBlocksById.set(item.id, currentBlock);\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\tcontent: currentBlock.thinking,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t\tcurrentBlock = null;\n\t\t\t} else if (item.type === \"message\" && currentBlock?.type === \"text\") {\n\t\t\t\tcurrentBlock.text = item.content.map((c) => (c.type === \"output_text\" ? c.text : c.refusal)).join(\"\");\n\t\t\t\tcurrentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\tcontentIndex: blockIndex(),\n\t\t\t\t\tcontent: currentBlock.text,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t\tcurrentBlock = null;\n\t\t\t} else if (item.type === \"function_call\") {\n\t\t\t\tconst args =\n\t\t\t\t\tcurrentBlock?.type === \"toolCall\" && currentBlock.partialJson\n\t\t\t\t\t\t? parseStreamingJson(currentBlock.partialJson)\n\t\t\t\t\t\t: parseStreamingJson(item.arguments || \"{}\");\n\n\t\t\t\tlet toolCall: ToolCall;\n\t\t\t\tif (currentBlock?.type === \"toolCall\") {\n\t\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only\n\t\t\t\t\t// carries parsed arguments.\n\t\t\t\t\tcurrentBlock.arguments = args;\n\t\t\t\t\tdelete (currentBlock as { partialJson?: string }).partialJson;\n\t\t\t\t\ttoolCall = currentBlock;\n\t\t\t\t} else {\n\t\t\t\t\ttoolCall = {\n\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\tid: `${item.call_id}|${item.id}`,\n\t\t\t\t\t\tname: item.name,\n\t\t\t\t\t\targuments: args,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tcurrentBlock = null;\n\t\t\t\tstream.push({ type: \"toolcall_end\", contentIndex: blockIndex(), toolCall, partial: output });\n\t\t\t}\n\t\t} else if (event.type === \"response.completed\" || event.type === \"response.incomplete\") {\n\t\t\tterminalEventSeen = true;\n\t\t\tconst response = event.response;\n\t\t\tbackfillReasoningSignatures(\n\t\t\t\tresponse.output?.filter((item): item is ResponseReasoningItem => item.type === \"reasoning\") ?? [],\n\t\t\t);\n\t\t\tif (response?.id) {\n\t\t\t\toutput.responseId = response.id;\n\t\t\t}\n\t\t\tif (response?.usage) {\n\t\t\t\tconst cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;\n\t\t\t\toutput.usage = {\n\t\t\t\t\t// OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input\n\t\t\t\t\tinput: (response.usage.input_tokens || 0) - cachedTokens,\n\t\t\t\t\toutput: response.usage.output_tokens || 0,\n\t\t\t\t\tcacheRead: cachedTokens,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: response.usage.total_tokens || 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tcalculateCost(model, output.usage);\n\t\t\tif (options?.applyServiceTierPricing) {\n\t\t\t\tconst serviceTier = options.resolveServiceTier\n\t\t\t\t\t? options.resolveServiceTier(response?.service_tier, options.serviceTier)\n\t\t\t\t\t: (response?.service_tier ?? options.serviceTier);\n\t\t\t\toptions.applyServiceTierPricing(output.usage, serviceTier);\n\t\t\t}\n\t\t\t// Map status to stop reason\n\t\t\toutput.stopReason = mapStopReason(response?.status);\n\t\t\tif (output.content.some((b) => b.type === \"toolCall\") && output.stopReason === \"stop\") {\n\t\t\t\toutput.stopReason = \"toolUse\";\n\t\t\t}\n\t\t} else if (event.type === \"error\") {\n\t\t\tthrow new Error(`Error Code ${event.code}: ${event.message}` || \"Unknown error\");\n\t\t} else if (event.type === \"response.failed\") {\n\t\t\tterminalEventSeen = true;\n\t\t\tconst error = event.response?.error;\n\t\t\tconst details = event.response?.incomplete_details;\n\t\t\tconst msg = error\n\t\t\t\t? `${error.code || \"unknown\"}: ${error.message || \"no message\"}`\n\t\t\t\t: details?.reason\n\t\t\t\t\t? `incomplete: ${details.reason}`\n\t\t\t\t\t: \"Unknown error (no error details in response)\";\n\t\t\tthrow new Error(msg);\n\t\t}\n\t}\n\n\tif (!terminalEventSeen) {\n\t\tthrow new Error(\"OpenAI Responses stream ended before a terminal response event\");\n\t}\n}\n\nfunction mapStopReason(status: OpenAI.Responses.ResponseStatus | undefined): StopReason {\n\tif (!status) return \"stop\";\n\tswitch (status) {\n\t\tcase \"completed\":\n\t\t\treturn \"stop\";\n\t\tcase \"incomplete\":\n\t\t\treturn \"length\";\n\t\tcase \"failed\":\n\t\tcase \"cancelled\":\n\t\t\treturn \"error\";\n\t\t// These two are wonky ...\n\t\tcase \"in_progress\":\n\t\tcase \"queued\":\n\t\t\treturn \"stop\";\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = status;\n\t\t\tthrow new Error(`Unhandled stop reason: ${_exhaustive}`);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"openai-responses-shared.d.ts","sourceRoot":"","sources":["../../src/providers/openai-responses-shared.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,IAAI,IAAI,UAAU,EAClB,6BAA6B,EAE7B,aAAa,EAQb,mBAAmB,EAEnB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAChB,OAAO,EAEP,KAAK,EAKL,IAAI,EAEJ,KAAK,EACL,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAqC5E,MAAM,WAAW,4BAA4B;IAC5C,WAAW,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC;IAC5D,kBAAkB,CAAC,EAAE,CACpB,mBAAmB,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,EAC9E,kBAAkB,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,KACzE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAC/D,uBAAuB,CAAC,EAAE,CACzB,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,6BAA6B,CAAC,cAAc,CAAC,GAAG,SAAS,KAClE,IAAI,CAAC;CACV;AAED,MAAM,WAAW,+BAA+B;IAC/C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,4BAA4B;IAC5C,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,wBAAwB,CAAC,IAAI,SAAS,GAAG,EACxD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAAC,EAC7C,OAAO,CAAC,EAAE,+BAA+B,GACvC,aAAa,CAoLf;AAMD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,4BAA4B,GAAG,UAAU,EAAE,CAUzG;AAeD,wBAAsB,sBAAsB,CAAC,IAAI,SAAS,GAAG,EAC5D,YAAY,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAChD,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,2BAA2B,EACnC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,CAAC,EAAE,4BAA4B,GACpC,OAAO,CAAC,IAAI,CAAC,CAoOf","sourcesContent":["import type OpenAI from \"openai\";\nimport type {\n\tTool as OpenAITool,\n\tResponseCreateParamsStreaming,\n\tResponseFunctionCallOutputItemList,\n\tResponseInput,\n\tResponseInputContent,\n\tResponseInputImage,\n\tResponseInputItem,\n\tResponseInputText,\n\tResponseOutputItem,\n\tResponseOutputMessage,\n\tResponseReasoningItem,\n\tResponseStreamEvent,\n\tResponseToolSearchOutputItemParam,\n} from \"openai/resources/responses/responses.js\";\nimport { calculateCost } from \"../models.js\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tImageContent,\n\tModel,\n\tStopReason,\n\tTextContent,\n\tTextSignatureV1,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tUsage,\n} from \"../types.js\";\nimport type { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport { shortHash } from \"../utils/hash.js\";\nimport { finalizeToolCallArguments, parseStreamingJson } from \"../utils/json-parse.js\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.js\";\nimport { createProviderToolCallIdNormalizer } from \"../utils/tool-call-id.js\";\nimport { transformMessages } from \"./transform-messages.js\";\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\nfunction encodeTextSignatureV1(id: string, phase?: TextSignatureV1[\"phase\"]): string {\n\tconst payload: TextSignatureV1 = { v: 1, id };\n\tif (phase) payload.phase = phase;\n\treturn JSON.stringify(payload);\n}\n\nfunction parseTextSignature(\n\tsignature: string | undefined,\n): { id: string; phase?: TextSignatureV1[\"phase\"] } | undefined {\n\tif (!signature) return undefined;\n\tif (signature.startsWith(\"{\")) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(signature) as Partial<TextSignatureV1>;\n\t\t\tif (parsed.v === 1 && typeof parsed.id === \"string\") {\n\t\t\t\tif (parsed.phase === \"commentary\" || parsed.phase === \"final_answer\") {\n\t\t\t\t\treturn { id: parsed.id, phase: parsed.phase };\n\t\t\t\t}\n\t\t\t\treturn { id: parsed.id };\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to legacy plain-string handling.\n\t\t}\n\t}\n\treturn { id: signature };\n}\n\nexport interface OpenAIResponsesStreamOptions {\n\tserviceTier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\tresolveServiceTier?: (\n\t\tresponseServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t\trequestServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t) => ResponseCreateParamsStreaming[\"service_tier\"] | undefined;\n\tapplyServiceTierPricing?: (\n\t\tusage: Usage,\n\t\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\t) => void;\n}\n\nexport interface ConvertResponsesMessagesOptions {\n\tincludeSystemPrompt?: boolean;\n\tdeferredTools?: ReadonlyMap<string, Tool>;\n}\n\nexport interface ConvertResponsesToolsOptions {\n\tstrict?: boolean | null;\n\tdeferLoading?: boolean;\n}\n\n// =============================================================================\n// Message conversion\n// =============================================================================\n\nexport function convertResponsesMessages<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\tallowedToolCallProviders: ReadonlySet<string>,\n\toptions?: ConvertResponsesMessagesOptions,\n): ResponseInput {\n\tconst messages: ResponseInput = [];\n\tconst loadedToolNames = new Set<string>();\n\n\tconst normalizeToolCallId = createProviderToolCallIdNormalizer(model, allowedToolCallProviders);\n\n\tconst transformedMessages = transformMessages(context.messages, model, (id, _model, source) =>\n\t\tnormalizeToolCallId(id, source),\n\t);\n\n\tconst includeSystemPrompt = options?.includeSystemPrompt ?? true;\n\tif (includeSystemPrompt && context.systemPrompt) {\n\t\tconst compat = model.compat as { supportsDeveloperRole?: boolean } | undefined;\n\t\tconst role = model.reasoning && compat?.supportsDeveloperRole !== false ? \"developer\" : \"system\";\n\t\tmessages.push({\n\t\t\trole,\n\t\t\tcontent: sanitizeSurrogates(context.systemPrompt),\n\t\t});\n\t}\n\n\tlet msgIndex = 0;\n\tfor (const msg of transformedMessages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tmessages.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"input_text\", text: sanitizeSurrogates(msg.content) }],\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"input_text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t} satisfies ResponseInputText;\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"input_image\",\n\t\t\t\t\t\tdetail: \"auto\",\n\t\t\t\t\t\timage_url: `data:${item.mimeType};base64,${item.data}`,\n\t\t\t\t\t} satisfies ResponseInputImage;\n\t\t\t\t});\n\t\t\t\tif (content.length === 0) continue;\n\t\t\t\tmessages.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst output: ResponseInput = [];\n\t\t\tconst assistantMsg = msg as AssistantMessage;\n\t\t\tconst isSameProviderAndApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api;\n\t\t\tconst isSameModel = isSameProviderAndApi && assistantMsg.model === model.id;\n\t\t\tconst isDifferentModel = isSameProviderAndApi && assistantMsg.model !== model.id;\n\t\t\tlet textBlockIndex = 0;\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"thinking\") {\n\t\t\t\t\tif (block.thinkingSignature) {\n\t\t\t\t\t\tconst reasoningItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;\n\t\t\t\t\t\toutput.push(reasoningItem);\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"text\") {\n\t\t\t\t\tconst textBlock = block as TextContent;\n\t\t\t\t\tconst parsedSignature = parseTextSignature(textBlock.textSignature);\n\t\t\t\t\tconst fallbackMessageId =\n\t\t\t\t\t\ttextBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`;\n\t\t\t\t\ttextBlockIndex++;\n\t\t\t\t\t// OpenAI requires id to be max 64 characters\n\t\t\t\t\tlet msgId = parsedSignature?.id;\n\t\t\t\t\tif (!msgId) {\n\t\t\t\t\t\tmsgId = fallbackMessageId;\n\t\t\t\t\t} else if (msgId.length > 64) {\n\t\t\t\t\t\tmsgId = `msg_${shortHash(msgId)}`;\n\t\t\t\t\t}\n\t\t\t\t\toutput.push({\n\t\t\t\t\t\ttype: \"message\",\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tcontent: [{ type: \"output_text\", text: sanitizeSurrogates(textBlock.text), annotations: [] }],\n\t\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\t\tid: msgId,\n\t\t\t\t\t\tphase: parsedSignature?.phase,\n\t\t\t\t\t} satisfies ResponseOutputMessage);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst toolCall = block as ToolCall;\n\t\t\t\t\tconst [callId, itemIdRaw] = toolCall.id.split(\"|\");\n\t\t\t\t\tlet itemId: string | undefined = itemIdRaw;\n\n\t\t\t\t\t// For different-model messages, set id to undefined to avoid pairing validation.\n\t\t\t\t\t// OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items.\n\t\t\t\t\t// By omitting the id, we avoid triggering that validation (like cross-provider does).\n\t\t\t\t\tif (isDifferentModel && itemId?.startsWith(\"fc_\")) {\n\t\t\t\t\t\titemId = undefined;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;\n\n\t\t\t\t\toutput.push({\n\t\t\t\t\t\ttype: \"function_call\",\n\t\t\t\t\t\tid: itemId,\n\t\t\t\t\t\tcall_id: callId,\n\t\t\t\t\t\tname: toolCall.name,\n\t\t\t\t\t\targuments: JSON.stringify(toolCall.arguments),\n\t\t\t\t\t\t...(canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (output.length === 0) continue;\n\t\t\tmessages.push(...output);\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst textResult = msg.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst hasImages = msg.content.some((c): c is ImageContent => c.type === \"image\");\n\t\t\tconst hasText = textResult.length > 0;\n\t\t\tconst [callId] = msg.toolCallId.split(\"|\");\n\n\t\t\tlet output: string | ResponseFunctionCallOutputItemList;\n\t\t\tif (hasImages && model.input.includes(\"image\")) {\n\t\t\t\tconst contentParts: ResponseFunctionCallOutputItemList = [];\n\n\t\t\t\tif (hasText) {\n\t\t\t\t\tcontentParts.push({\n\t\t\t\t\t\ttype: \"input_text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(textResult),\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const block of msg.content) {\n\t\t\t\t\tif (block.type === \"image\") {\n\t\t\t\t\t\tcontentParts.push({\n\t\t\t\t\t\t\ttype: \"input_image\",\n\t\t\t\t\t\t\tdetail: \"auto\",\n\t\t\t\t\t\t\timage_url: `data:${block.mimeType};base64,${block.data}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\toutput = contentParts;\n\t\t\t} else {\n\t\t\t\toutput = sanitizeSurrogates(hasText ? textResult : hasImages ? \"(see attached image)\" : \"(no tool output)\");\n\t\t\t}\n\n\t\t\tmessages.push({\n\t\t\t\ttype: \"function_call_output\",\n\t\t\t\tcall_id: callId,\n\t\t\t\toutput,\n\t\t\t});\n\n\t\t\tconst deferredTools: Tool[] = [];\n\t\t\tfor (const name of msg.addedToolNames ?? []) {\n\t\t\t\tconst tool = options?.deferredTools?.get(name);\n\t\t\t\tif (!tool || loadedToolNames.has(name)) continue;\n\t\t\t\tloadedToolNames.add(name);\n\t\t\t\tdeferredTools.push(tool);\n\t\t\t}\n\t\t\tif (deferredTools.length > 0) {\n\t\t\t\tconst names = deferredTools.map((tool) => tool.name);\n\t\t\t\tconst searchCallId = `pi_tool_load_${shortHash(`${msg.toolCallId}:${names.join(\",\")}`)}`;\n\t\t\t\tmessages.push({\n\t\t\t\t\ttype: \"tool_search_call\",\n\t\t\t\t\tcall_id: searchCallId,\n\t\t\t\t\texecution: \"client\",\n\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\targuments: { query: names.join(\" \"), limit: names.length },\n\t\t\t\t} satisfies ResponseInputItem);\n\t\t\t\tmessages.push({\n\t\t\t\t\ttype: \"tool_search_output\",\n\t\t\t\t\tcall_id: searchCallId,\n\t\t\t\t\texecution: \"client\",\n\t\t\t\t\tstatus: \"completed\",\n\t\t\t\t\ttools: convertResponsesTools(deferredTools, { deferLoading: true }),\n\t\t\t\t} satisfies ResponseToolSearchOutputItemParam);\n\t\t\t}\n\t\t}\n\t\tmsgIndex++;\n\t}\n\n\treturn messages;\n}\n\n// =============================================================================\n// Tool conversion\n// =============================================================================\n\nexport function convertResponsesTools(tools: Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] {\n\tconst strict = options?.strict === undefined ? false : options.strict;\n\treturn tools.map((tool) => ({\n\t\ttype: \"function\",\n\t\tname: tool.name,\n\t\tdescription: tool.description,\n\t\tparameters: tool.parameters as any, // TypeBox already generates JSON Schema\n\t\tstrict,\n\t\t...(options?.deferLoading ? { defer_loading: true } : {}),\n\t}));\n}\n\n// =============================================================================\n// Stream processing\n// =============================================================================\n\ntype StreamingToolCall = ToolCall & { partialJson: string };\n\n// Responses can interleave events for several output items, so each item keeps its own slot keyed by\n// output_index instead of a single \"current\" block.\ntype ResponsesOutputSlot =\n\t| { type: \"thinking\"; block: ThinkingContent; contentIndex: number }\n\t| { type: \"text\"; block: TextContent; contentIndex: number }\n\t| { type: \"toolCall\"; block: StreamingToolCall; contentIndex: number };\n\nexport async function processResponsesStream<TApi extends Api>(\n\topenaiStream: AsyncIterable<ResponseStreamEvent>,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<TApi>,\n\toptions?: OpenAIResponsesStreamOptions,\n): Promise<void> {\n\tlet terminalEventSeen = false;\n\tconst reasoningBlocksById = new Map<string, ThinkingContent>();\n\tconst outputSlots = new Map<number, ResponsesOutputSlot>();\n\tconst getSlot = <TType extends ResponsesOutputSlot[\"type\"]>(\n\t\toutputIndex: number,\n\t\ttype: TType,\n\t): Extract<ResponsesOutputSlot, { type: TType }> | undefined => {\n\t\tconst slot = outputSlots.get(outputIndex);\n\t\treturn slot?.type === type ? (slot as Extract<ResponsesOutputSlot, { type: TType }>) : undefined;\n\t};\n\tconst createSlot = (outputIndex: number, item: ResponseOutputItem): ResponsesOutputSlot | undefined => {\n\t\tif (item.type === \"reasoning\") {\n\t\t\tconst block: ThinkingContent = { type: \"thinking\", thinking: \"\" };\n\t\t\toutput.content.push(block);\n\t\t\tconst slot = {\n\t\t\t\ttype: \"thinking\",\n\t\t\t\tblock,\n\t\t\t\tcontentIndex: output.content.length - 1,\n\t\t\t} satisfies ResponsesOutputSlot;\n\t\t\toutputSlots.set(outputIndex, slot);\n\t\t\tstream.push({ type: \"thinking_start\", contentIndex: slot.contentIndex, partial: output });\n\t\t\treturn slot;\n\t\t}\n\t\tif (item.type === \"message\") {\n\t\t\tconst block: TextContent = { type: \"text\", text: \"\" };\n\t\t\toutput.content.push(block);\n\t\t\tconst slot = { type: \"text\", block, contentIndex: output.content.length - 1 } satisfies ResponsesOutputSlot;\n\t\t\toutputSlots.set(outputIndex, slot);\n\t\t\tstream.push({ type: \"text_start\", contentIndex: slot.contentIndex, partial: output });\n\t\t\treturn slot;\n\t\t}\n\t\tif (item.type === \"function_call\") {\n\t\t\tconst block: StreamingToolCall = {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: `${item.call_id}|${item.id}`,\n\t\t\t\tname: item.name,\n\t\t\t\targuments: {},\n\t\t\t\t...(item.namespace !== undefined ? { namespace: item.namespace } : {}),\n\t\t\t\tpartialJson: item.arguments || \"\",\n\t\t\t};\n\t\t\toutput.content.push(block);\n\t\t\tconst slot = {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tblock,\n\t\t\t\tcontentIndex: output.content.length - 1,\n\t\t\t} satisfies ResponsesOutputSlot;\n\t\t\toutputSlots.set(outputIndex, slot);\n\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: slot.contentIndex, partial: output });\n\t\t\treturn slot;\n\t\t}\n\t\treturn undefined;\n\t};\n\tconst getOrCreateSlot = (outputIndex: number, item: ResponseOutputItem): ResponsesOutputSlot | undefined =>\n\t\toutputSlots.get(outputIndex) ?? createSlot(outputIndex, item);\n\tconst backfillReasoningSignatures = (responseOutput: ResponseReasoningItem[]): void => {\n\t\tfor (const item of responseOutput) {\n\t\t\tif (!item.encrypted_content) continue;\n\t\t\tconst block = reasoningBlocksById.get(item.id);\n\t\t\tif (!block?.thinkingSignature) continue;\n\n\t\t\tconst storedItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;\n\t\t\tif (storedItem.encrypted_content) continue;\n\t\t\tblock.thinkingSignature = JSON.stringify({ ...storedItem, encrypted_content: item.encrypted_content });\n\t\t}\n\t};\n\n\tlet unfinishedToolCall: ToolCall | undefined;\n\tconst finalizeOutstandingToolCalls = (): void => {\n\t\tfor (const block of output.content) {\n\t\t\tif (block.type !== \"toolCall\") continue;\n\t\t\tconst call = block as ToolCall & { partialJson?: string };\n\t\t\tif (call.partialJson === undefined) continue;\n\t\t\tunfinishedToolCall ??= block;\n\t\t\tfinalizeToolCallArguments(call, call.partialJson);\n\t\t\tdelete call.partialJson;\n\t\t}\n\t};\n\n\tfor await (const event of openaiStream) {\n\t\tif (event.type === \"response.created\") {\n\t\t\toutput.responseId = event.response.id;\n\t\t} else if (event.type === \"response.output_item.added\") {\n\t\t\tcreateSlot(event.output_index, event.item);\n\t\t} else if (event.type === \"response.reasoning_summary_text.delta\") {\n\t\t\tconst slot = getSlot(event.output_index, \"thinking\");\n\t\t\tif (!slot) continue;\n\t\t\tslot.block.thinking += event.delta;\n\t\t\tstream.push({ type: \"thinking_delta\", contentIndex: slot.contentIndex, delta: event.delta, partial: output });\n\t\t} else if (event.type === \"response.reasoning_summary_part.done\") {\n\t\t\tconst slot = getSlot(event.output_index, \"thinking\");\n\t\t\tif (!slot) continue;\n\t\t\tslot.block.thinking += \"\\n\\n\";\n\t\t\tstream.push({ type: \"thinking_delta\", contentIndex: slot.contentIndex, delta: \"\\n\\n\", partial: output });\n\t\t} else if (event.type === \"response.reasoning_text.delta\") {\n\t\t\tconst slot = getSlot(event.output_index, \"thinking\");\n\t\t\tif (!slot) continue;\n\t\t\tslot.block.thinking += event.delta;\n\t\t\tstream.push({ type: \"thinking_delta\", contentIndex: slot.contentIndex, delta: event.delta, partial: output });\n\t\t} else if (event.type === \"response.output_text.delta\" || event.type === \"response.refusal.delta\") {\n\t\t\tconst slot = getSlot(event.output_index, \"text\");\n\t\t\tif (!slot) continue;\n\t\t\tslot.block.text += event.delta;\n\t\t\tstream.push({ type: \"text_delta\", contentIndex: slot.contentIndex, delta: event.delta, partial: output });\n\t\t} else if (event.type === \"response.function_call_arguments.delta\") {\n\t\t\tconst slot = getSlot(event.output_index, \"toolCall\");\n\t\t\tif (!slot) continue;\n\t\t\tslot.block.partialJson += event.delta;\n\t\t\tslot.block.arguments = parseStreamingJson(slot.block.partialJson);\n\t\t\tstream.push({ type: \"toolcall_delta\", contentIndex: slot.contentIndex, delta: event.delta, partial: output });\n\t\t} else if (event.type === \"response.function_call_arguments.done\") {\n\t\t\tconst slot = getSlot(event.output_index, \"toolCall\");\n\t\t\tif (!slot) continue;\n\t\t\tconst previousPartialJson = slot.block.partialJson;\n\t\t\tslot.block.partialJson = event.arguments;\n\t\t\tfinalizeToolCallArguments(slot.block, slot.block.partialJson);\n\n\t\t\tif (event.arguments.startsWith(previousPartialJson)) {\n\t\t\t\tconst delta = event.arguments.slice(previousPartialJson.length);\n\t\t\t\tif (delta.length > 0) {\n\t\t\t\t\tstream.push({ type: \"toolcall_delta\", contentIndex: slot.contentIndex, delta, partial: output });\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (event.type === \"response.output_item.done\") {\n\t\t\tconst item = event.item;\n\t\t\tconst slot = getOrCreateSlot(event.output_index, item);\n\n\t\t\tif (item.type === \"reasoning\" && slot?.type === \"thinking\") {\n\t\t\t\tconst summaryText = item.summary?.map((s) => s.text).join(\"\\n\\n\") || \"\";\n\t\t\t\tconst contentText = item.content?.map((c) => c.text).join(\"\\n\\n\") || \"\";\n\t\t\t\tslot.block.thinking = summaryText || contentText || slot.block.thinking;\n\t\t\t\tslot.block.thinkingSignature = JSON.stringify(item);\n\t\t\t\treasoningBlocksById.set(item.id, slot.block);\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\tcontentIndex: slot.contentIndex,\n\t\t\t\t\tcontent: slot.block.thinking,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t\toutputSlots.delete(event.output_index);\n\t\t\t} else if (item.type === \"message\" && slot?.type === \"text\") {\n\t\t\t\tslot.block.text = item.content?.map((c) => (c.type === \"output_text\" ? c.text : c.refusal)).join(\"\") || \"\";\n\t\t\t\tslot.block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\tcontentIndex: slot.contentIndex,\n\t\t\t\t\tcontent: slot.block.text,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t\toutputSlots.delete(event.output_index);\n\t\t\t} else if (item.type === \"function_call\" && slot?.type === \"toolCall\") {\n\t\t\t\t// The done item's arguments are authoritative; streamed deltas are only a fallback.\n\t\t\t\tfinalizeToolCallArguments(slot.block, item.arguments ?? slot.block.partialJson);\n\t\t\t\tif (item.namespace !== undefined) slot.block.namespace = item.namespace;\n\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only carries parsed arguments.\n\t\t\t\tdelete (slot.block as { partialJson?: string }).partialJson;\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"toolcall_end\",\n\t\t\t\t\tcontentIndex: slot.contentIndex,\n\t\t\t\t\ttoolCall: slot.block,\n\t\t\t\t\tpartial: output,\n\t\t\t\t});\n\t\t\t\toutputSlots.delete(event.output_index);\n\t\t\t}\n\t\t} else if (event.type === \"response.completed\" || event.type === \"response.incomplete\") {\n\t\t\tterminalEventSeen = true;\n\t\t\tfinalizeOutstandingToolCalls();\n\t\t\tconst response = event.response;\n\t\t\tbackfillReasoningSignatures(\n\t\t\t\tresponse.output?.filter((item): item is ResponseReasoningItem => item.type === \"reasoning\") ?? [],\n\t\t\t);\n\t\t\tif (response?.id) {\n\t\t\t\toutput.responseId = response.id;\n\t\t\t}\n\t\t\tif (response?.usage) {\n\t\t\t\tconst cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;\n\t\t\t\toutput.usage = {\n\t\t\t\t\t// OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input\n\t\t\t\t\tinput: (response.usage.input_tokens || 0) - cachedTokens,\n\t\t\t\t\toutput: response.usage.output_tokens || 0,\n\t\t\t\t\tcacheRead: cachedTokens,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotalTokens: response.usage.total_tokens || 0,\n\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tcalculateCost(model, output.usage);\n\t\t\tif (options?.applyServiceTierPricing) {\n\t\t\t\tconst serviceTier = options.resolveServiceTier\n\t\t\t\t\t? options.resolveServiceTier(response?.service_tier, options.serviceTier)\n\t\t\t\t\t: (response?.service_tier ?? options.serviceTier);\n\t\t\t\toptions.applyServiceTierPricing(output.usage, serviceTier);\n\t\t\t}\n\t\t\t// Map status to stop reason\n\t\t\tconst status = response?.status;\n\t\t\toutput.rawStopReason = status;\n\t\t\toutput.stopReason = mapStopReason(status);\n\t\t\tif (output.content.some((b) => b.type === \"toolCall\") && output.stopReason === \"stop\") {\n\t\t\t\toutput.stopReason = \"toolUse\";\n\t\t\t}\n\t\t} else if (event.type === \"error\") {\n\t\t\tthrow Object.assign(new Error(`Error Code ${event.code}: ${event.message}` || \"Unknown error\"), {\n\t\t\t\tcode: event.code,\n\t\t\t});\n\t\t} else if (event.type === \"response.failed\") {\n\t\t\tterminalEventSeen = true;\n\t\t\toutput.rawStopReason = event.response?.status;\n\t\t\tconst error = event.response?.error;\n\t\t\tconst details = event.response?.incomplete_details;\n\t\t\tconst msg = error\n\t\t\t\t? `${error.code || \"unknown\"}: ${error.message || \"no message\"}`\n\t\t\t\t: details?.reason\n\t\t\t\t\t? `incomplete: ${details.reason}`\n\t\t\t\t\t: \"Unknown error (no error details in response)\";\n\t\t\tthrow Object.assign(new Error(msg), { code: error?.code });\n\t\t}\n\t}\n\n\tfinalizeOutstandingToolCalls();\n\tif (!terminalEventSeen) {\n\t\tthrow new Error(\"OpenAI Responses stream ended before a terminal response event\");\n\t}\n\t// Preserve the existing fail-closed guard even after stripping unfinished scratch buffers.\n\tif (output.stopReason === \"toolUse\" && unfinishedToolCall) {\n\t\tthrow new Error(\n\t\t\t`OpenAI Responses stream completed with an unfinished tool call: ${unfinishedToolCall.name} (${unfinishedToolCall.id})`,\n\t\t);\n\t}\n}\n\nfunction mapStopReason(status: OpenAI.Responses.ResponseStatus | undefined): StopReason {\n\tif (!status) return \"stop\";\n\tswitch (status) {\n\t\tcase \"completed\":\n\t\t\treturn \"stop\";\n\t\tcase \"incomplete\":\n\t\t\treturn \"length\";\n\t\tcase \"failed\":\n\t\tcase \"cancelled\":\n\t\t\treturn \"error\";\n\t\t// These two are wonky ...\n\t\tcase \"in_progress\":\n\t\tcase \"queued\":\n\t\t\treturn \"stop\";\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = status;\n\t\t\tthrow new Error(`Unhandled stop reason: ${_exhaustive}`);\n\t\t}\n\t}\n}\n"]}
@@ -1,7 +1,8 @@
1
1
  import { calculateCost } from "../models.js";
2
2
  import { shortHash } from "../utils/hash.js";
3
- import { parseStreamingJson } from "../utils/json-parse.js";
3
+ import { finalizeToolCallArguments, parseStreamingJson } from "../utils/json-parse.js";
4
4
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
5
+ import { createProviderToolCallIdNormalizer } from "../utils/tool-call-id.js";
5
6
  import { transformMessages } from "./transform-messages.js";
6
7
  // =============================================================================
7
8
  // Utilities
@@ -37,31 +38,8 @@ function parseTextSignature(signature) {
37
38
  export function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
38
39
  const messages = [];
39
40
  const loadedToolNames = new Set();
40
- const normalizeIdPart = (part) => {
41
- const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
42
- const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
43
- return normalized.replace(/_+$/, "");
44
- };
45
- const buildForeignResponsesItemId = (itemId) => {
46
- const normalized = `fc_${shortHash(itemId)}`;
47
- return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
48
- };
49
- const normalizeToolCallId = (id, _targetModel, source) => {
50
- if (!allowedToolCallProviders.has(model.provider))
51
- return normalizeIdPart(id);
52
- if (!id.includes("|"))
53
- return normalizeIdPart(id);
54
- const [callId, itemId] = id.split("|");
55
- const normalizedCallId = normalizeIdPart(callId);
56
- const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
57
- let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);
58
- // OpenAI Responses API requires item id to start with "fc"
59
- if (!normalizedItemId.startsWith("fc_")) {
60
- normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
61
- }
62
- return `${normalizedCallId}|${normalizedItemId}`;
63
- };
64
- const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
41
+ const normalizeToolCallId = createProviderToolCallIdNormalizer(model, allowedToolCallProviders);
42
+ const transformedMessages = transformMessages(context.messages, model, (id, _model, source) => normalizeToolCallId(id, source));
65
43
  const includeSystemPrompt = options?.includeSystemPrompt ?? true;
66
44
  if (includeSystemPrompt && context.systemPrompt) {
67
45
  const compat = model.compat;
@@ -105,9 +83,10 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
105
83
  else if (msg.role === "assistant") {
106
84
  const output = [];
107
85
  const assistantMsg = msg;
108
- const isDifferentModel = assistantMsg.model !== model.id &&
109
- assistantMsg.provider === model.provider &&
110
- assistantMsg.api === model.api;
86
+ const isSameProviderAndApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api;
87
+ const isSameModel = isSameProviderAndApi && assistantMsg.model === model.id;
88
+ const isDifferentModel = isSameProviderAndApi && assistantMsg.model !== model.id;
89
+ let textBlockIndex = 0;
111
90
  for (const block of msg.content) {
112
91
  if (block.type === "thinking") {
113
92
  if (block.thinkingSignature) {
@@ -118,10 +97,12 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
118
97
  else if (block.type === "text") {
119
98
  const textBlock = block;
120
99
  const parsedSignature = parseTextSignature(textBlock.textSignature);
100
+ const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`;
101
+ textBlockIndex++;
121
102
  // OpenAI requires id to be max 64 characters
122
103
  let msgId = parsedSignature?.id;
123
104
  if (!msgId) {
124
- msgId = `msg_${msgIndex}`;
105
+ msgId = fallbackMessageId;
125
106
  }
126
107
  else if (msgId.length > 64) {
127
108
  msgId = `msg_${shortHash(msgId)}`;
@@ -145,12 +126,14 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
145
126
  if (isDifferentModel && itemId?.startsWith("fc_")) {
146
127
  itemId = undefined;
147
128
  }
129
+ const canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;
148
130
  output.push({
149
131
  type: "function_call",
150
132
  id: itemId,
151
133
  call_id: callId,
152
134
  name: toolCall.name,
153
135
  arguments: JSON.stringify(toolCall.arguments),
136
+ ...(canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}),
154
137
  });
155
138
  }
156
139
  }
@@ -239,16 +222,57 @@ export function convertResponsesTools(tools, options) {
239
222
  ...(options?.deferLoading ? { defer_loading: true } : {}),
240
223
  }));
241
224
  }
242
- // =============================================================================
243
- // Stream processing
244
- // =============================================================================
245
225
  export async function processResponsesStream(openaiStream, output, stream, model, options) {
246
- let currentItem = null;
247
- let currentBlock = null;
248
226
  let terminalEventSeen = false;
249
227
  const reasoningBlocksById = new Map();
250
- const blocks = output.content;
251
- const blockIndex = () => blocks.length - 1;
228
+ const outputSlots = new Map();
229
+ const getSlot = (outputIndex, type) => {
230
+ const slot = outputSlots.get(outputIndex);
231
+ return slot?.type === type ? slot : undefined;
232
+ };
233
+ const createSlot = (outputIndex, item) => {
234
+ if (item.type === "reasoning") {
235
+ const block = { type: "thinking", thinking: "" };
236
+ output.content.push(block);
237
+ const slot = {
238
+ type: "thinking",
239
+ block,
240
+ contentIndex: output.content.length - 1,
241
+ };
242
+ outputSlots.set(outputIndex, slot);
243
+ stream.push({ type: "thinking_start", contentIndex: slot.contentIndex, partial: output });
244
+ return slot;
245
+ }
246
+ if (item.type === "message") {
247
+ const block = { type: "text", text: "" };
248
+ output.content.push(block);
249
+ const slot = { type: "text", block, contentIndex: output.content.length - 1 };
250
+ outputSlots.set(outputIndex, slot);
251
+ stream.push({ type: "text_start", contentIndex: slot.contentIndex, partial: output });
252
+ return slot;
253
+ }
254
+ if (item.type === "function_call") {
255
+ const block = {
256
+ type: "toolCall",
257
+ id: `${item.call_id}|${item.id}`,
258
+ name: item.name,
259
+ arguments: {},
260
+ ...(item.namespace !== undefined ? { namespace: item.namespace } : {}),
261
+ partialJson: item.arguments || "",
262
+ };
263
+ output.content.push(block);
264
+ const slot = {
265
+ type: "toolCall",
266
+ block,
267
+ contentIndex: output.content.length - 1,
268
+ };
269
+ outputSlots.set(outputIndex, slot);
270
+ stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output });
271
+ return slot;
272
+ }
273
+ return undefined;
274
+ };
275
+ const getOrCreateSlot = (outputIndex, item) => outputSlots.get(outputIndex) ?? createSlot(outputIndex, item);
252
276
  const backfillReasoningSignatures = (responseOutput) => {
253
277
  for (const item of responseOutput) {
254
278
  if (!item.encrypted_content)
@@ -262,214 +286,123 @@ export async function processResponsesStream(openaiStream, output, stream, model
262
286
  block.thinkingSignature = JSON.stringify({ ...storedItem, encrypted_content: item.encrypted_content });
263
287
  }
264
288
  };
289
+ let unfinishedToolCall;
290
+ const finalizeOutstandingToolCalls = () => {
291
+ for (const block of output.content) {
292
+ if (block.type !== "toolCall")
293
+ continue;
294
+ const call = block;
295
+ if (call.partialJson === undefined)
296
+ continue;
297
+ unfinishedToolCall ??= block;
298
+ finalizeToolCallArguments(call, call.partialJson);
299
+ delete call.partialJson;
300
+ }
301
+ };
265
302
  for await (const event of openaiStream) {
266
303
  if (event.type === "response.created") {
267
304
  output.responseId = event.response.id;
268
305
  }
269
306
  else if (event.type === "response.output_item.added") {
270
- const item = event.item;
271
- if (item.type === "reasoning") {
272
- currentItem = item;
273
- currentBlock = { type: "thinking", thinking: "" };
274
- output.content.push(currentBlock);
275
- stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
276
- }
277
- else if (item.type === "message") {
278
- currentItem = item;
279
- currentBlock = { type: "text", text: "" };
280
- output.content.push(currentBlock);
281
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
282
- }
283
- else if (item.type === "function_call") {
284
- currentItem = item;
285
- currentBlock = {
286
- type: "toolCall",
287
- id: `${item.call_id}|${item.id}`,
288
- name: item.name,
289
- arguments: {},
290
- partialJson: item.arguments || "",
291
- };
292
- output.content.push(currentBlock);
293
- stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
294
- }
295
- }
296
- else if (event.type === "response.reasoning_summary_part.added") {
297
- if (currentItem && currentItem.type === "reasoning") {
298
- currentItem.summary = currentItem.summary || [];
299
- currentItem.summary.push(event.part);
300
- }
307
+ createSlot(event.output_index, event.item);
301
308
  }
302
309
  else if (event.type === "response.reasoning_summary_text.delta") {
303
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
304
- currentItem.summary = currentItem.summary || [];
305
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
306
- if (lastPart) {
307
- currentBlock.thinking += event.delta;
308
- lastPart.text += event.delta;
309
- stream.push({
310
- type: "thinking_delta",
311
- contentIndex: blockIndex(),
312
- delta: event.delta,
313
- partial: output,
314
- });
315
- }
316
- }
310
+ const slot = getSlot(event.output_index, "thinking");
311
+ if (!slot)
312
+ continue;
313
+ slot.block.thinking += event.delta;
314
+ stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output });
317
315
  }
318
316
  else if (event.type === "response.reasoning_summary_part.done") {
319
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
320
- currentItem.summary = currentItem.summary || [];
321
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
322
- if (lastPart) {
323
- currentBlock.thinking += "\n\n";
324
- lastPart.text += "\n\n";
325
- stream.push({
326
- type: "thinking_delta",
327
- contentIndex: blockIndex(),
328
- delta: "\n\n",
329
- partial: output,
330
- });
331
- }
332
- }
317
+ const slot = getSlot(event.output_index, "thinking");
318
+ if (!slot)
319
+ continue;
320
+ slot.block.thinking += "\n\n";
321
+ stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: "\n\n", partial: output });
333
322
  }
334
323
  else if (event.type === "response.reasoning_text.delta") {
335
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
336
- currentBlock.thinking += event.delta;
337
- stream.push({
338
- type: "thinking_delta",
339
- contentIndex: blockIndex(),
340
- delta: event.delta,
341
- partial: output,
342
- });
343
- }
344
- }
345
- else if (event.type === "response.content_part.added") {
346
- if (currentItem?.type === "message") {
347
- currentItem.content = currentItem.content || [];
348
- // Filter out ReasoningText, only accept output_text and refusal
349
- if (event.part.type === "output_text" || event.part.type === "refusal") {
350
- currentItem.content.push(event.part);
351
- }
352
- }
353
- }
354
- else if (event.type === "response.output_text.delta") {
355
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
356
- if (!currentItem.content || currentItem.content.length === 0) {
357
- continue;
358
- }
359
- const lastPart = currentItem.content[currentItem.content.length - 1];
360
- if (lastPart?.type === "output_text") {
361
- currentBlock.text += event.delta;
362
- lastPart.text += event.delta;
363
- stream.push({
364
- type: "text_delta",
365
- contentIndex: blockIndex(),
366
- delta: event.delta,
367
- partial: output,
368
- });
369
- }
370
- }
324
+ const slot = getSlot(event.output_index, "thinking");
325
+ if (!slot)
326
+ continue;
327
+ slot.block.thinking += event.delta;
328
+ stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output });
371
329
  }
372
- else if (event.type === "response.refusal.delta") {
373
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
374
- if (!currentItem.content || currentItem.content.length === 0) {
375
- continue;
376
- }
377
- const lastPart = currentItem.content[currentItem.content.length - 1];
378
- if (lastPart?.type === "refusal") {
379
- currentBlock.text += event.delta;
380
- lastPart.refusal += event.delta;
381
- stream.push({
382
- type: "text_delta",
383
- contentIndex: blockIndex(),
384
- delta: event.delta,
385
- partial: output,
386
- });
387
- }
388
- }
330
+ else if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
331
+ const slot = getSlot(event.output_index, "text");
332
+ if (!slot)
333
+ continue;
334
+ slot.block.text += event.delta;
335
+ stream.push({ type: "text_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output });
389
336
  }
390
337
  else if (event.type === "response.function_call_arguments.delta") {
391
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
392
- currentBlock.partialJson += event.delta;
393
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
394
- stream.push({
395
- type: "toolcall_delta",
396
- contentIndex: blockIndex(),
397
- delta: event.delta,
398
- partial: output,
399
- });
400
- }
338
+ const slot = getSlot(event.output_index, "toolCall");
339
+ if (!slot)
340
+ continue;
341
+ slot.block.partialJson += event.delta;
342
+ slot.block.arguments = parseStreamingJson(slot.block.partialJson);
343
+ stream.push({ type: "toolcall_delta", contentIndex: slot.contentIndex, delta: event.delta, partial: output });
401
344
  }
402
345
  else if (event.type === "response.function_call_arguments.done") {
403
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
404
- const previousPartialJson = currentBlock.partialJson;
405
- currentBlock.partialJson = event.arguments;
406
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
407
- if (event.arguments.startsWith(previousPartialJson)) {
408
- const delta = event.arguments.slice(previousPartialJson.length);
409
- if (delta.length > 0) {
410
- stream.push({
411
- type: "toolcall_delta",
412
- contentIndex: blockIndex(),
413
- delta,
414
- partial: output,
415
- });
416
- }
346
+ const slot = getSlot(event.output_index, "toolCall");
347
+ if (!slot)
348
+ continue;
349
+ const previousPartialJson = slot.block.partialJson;
350
+ slot.block.partialJson = event.arguments;
351
+ finalizeToolCallArguments(slot.block, slot.block.partialJson);
352
+ if (event.arguments.startsWith(previousPartialJson)) {
353
+ const delta = event.arguments.slice(previousPartialJson.length);
354
+ if (delta.length > 0) {
355
+ stream.push({ type: "toolcall_delta", contentIndex: slot.contentIndex, delta, partial: output });
417
356
  }
418
357
  }
419
358
  }
420
359
  else if (event.type === "response.output_item.done") {
421
360
  const item = event.item;
422
- if (item.type === "reasoning" && currentBlock?.type === "thinking") {
361
+ const slot = getOrCreateSlot(event.output_index, item);
362
+ if (item.type === "reasoning" && slot?.type === "thinking") {
423
363
  const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
424
364
  const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
425
- currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
426
- currentBlock.thinkingSignature = JSON.stringify(item);
427
- reasoningBlocksById.set(item.id, currentBlock);
365
+ slot.block.thinking = summaryText || contentText || slot.block.thinking;
366
+ slot.block.thinkingSignature = JSON.stringify(item);
367
+ reasoningBlocksById.set(item.id, slot.block);
428
368
  stream.push({
429
369
  type: "thinking_end",
430
- contentIndex: blockIndex(),
431
- content: currentBlock.thinking,
370
+ contentIndex: slot.contentIndex,
371
+ content: slot.block.thinking,
432
372
  partial: output,
433
373
  });
434
- currentBlock = null;
374
+ outputSlots.delete(event.output_index);
435
375
  }
436
- else if (item.type === "message" && currentBlock?.type === "text") {
437
- currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
438
- currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
376
+ else if (item.type === "message" && slot?.type === "text") {
377
+ slot.block.text = item.content?.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("") || "";
378
+ slot.block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
439
379
  stream.push({
440
380
  type: "text_end",
441
- contentIndex: blockIndex(),
442
- content: currentBlock.text,
381
+ contentIndex: slot.contentIndex,
382
+ content: slot.block.text,
443
383
  partial: output,
444
384
  });
445
- currentBlock = null;
385
+ outputSlots.delete(event.output_index);
446
386
  }
447
- else if (item.type === "function_call") {
448
- const args = currentBlock?.type === "toolCall" && currentBlock.partialJson
449
- ? parseStreamingJson(currentBlock.partialJson)
450
- : parseStreamingJson(item.arguments || "{}");
451
- let toolCall;
452
- if (currentBlock?.type === "toolCall") {
453
- // Finalize in-place and strip the scratch buffer so replay only
454
- // carries parsed arguments.
455
- currentBlock.arguments = args;
456
- delete currentBlock.partialJson;
457
- toolCall = currentBlock;
458
- }
459
- else {
460
- toolCall = {
461
- type: "toolCall",
462
- id: `${item.call_id}|${item.id}`,
463
- name: item.name,
464
- arguments: args,
465
- };
466
- }
467
- currentBlock = null;
468
- stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
387
+ else if (item.type === "function_call" && slot?.type === "toolCall") {
388
+ // The done item's arguments are authoritative; streamed deltas are only a fallback.
389
+ finalizeToolCallArguments(slot.block, item.arguments ?? slot.block.partialJson);
390
+ if (item.namespace !== undefined)
391
+ slot.block.namespace = item.namespace;
392
+ // Finalize in-place and strip the scratch buffer so replay only carries parsed arguments.
393
+ delete slot.block.partialJson;
394
+ stream.push({
395
+ type: "toolcall_end",
396
+ contentIndex: slot.contentIndex,
397
+ toolCall: slot.block,
398
+ partial: output,
399
+ });
400
+ outputSlots.delete(event.output_index);
469
401
  }
470
402
  }
471
403
  else if (event.type === "response.completed" || event.type === "response.incomplete") {
472
404
  terminalEventSeen = true;
405
+ finalizeOutstandingToolCalls();
473
406
  const response = event.response;
474
407
  backfillReasoningSignatures(response.output?.filter((item) => item.type === "reasoning") ?? []);
475
408
  if (response?.id) {
@@ -495,16 +428,21 @@ export async function processResponsesStream(openaiStream, output, stream, model
495
428
  options.applyServiceTierPricing(output.usage, serviceTier);
496
429
  }
497
430
  // Map status to stop reason
498
- output.stopReason = mapStopReason(response?.status);
431
+ const status = response?.status;
432
+ output.rawStopReason = status;
433
+ output.stopReason = mapStopReason(status);
499
434
  if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
500
435
  output.stopReason = "toolUse";
501
436
  }
502
437
  }
503
438
  else if (event.type === "error") {
504
- throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
439
+ throw Object.assign(new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error"), {
440
+ code: event.code,
441
+ });
505
442
  }
506
443
  else if (event.type === "response.failed") {
507
444
  terminalEventSeen = true;
445
+ output.rawStopReason = event.response?.status;
508
446
  const error = event.response?.error;
509
447
  const details = event.response?.incomplete_details;
510
448
  const msg = error
@@ -512,12 +450,17 @@ export async function processResponsesStream(openaiStream, output, stream, model
512
450
  : details?.reason
513
451
  ? `incomplete: ${details.reason}`
514
452
  : "Unknown error (no error details in response)";
515
- throw new Error(msg);
453
+ throw Object.assign(new Error(msg), { code: error?.code });
516
454
  }
517
455
  }
456
+ finalizeOutstandingToolCalls();
518
457
  if (!terminalEventSeen) {
519
458
  throw new Error("OpenAI Responses stream ended before a terminal response event");
520
459
  }
460
+ // Preserve the existing fail-closed guard even after stripping unfinished scratch buffers.
461
+ if (output.stopReason === "toolUse" && unfinishedToolCall) {
462
+ throw new Error(`OpenAI Responses stream completed with an unfinished tool call: ${unfinishedToolCall.name} (${unfinishedToolCall.id})`);
463
+ }
521
464
  }
522
465
  function mapStopReason(status) {
523
466
  if (!status)