@fgv/ts-extras 5.1.0-54 → 5.1.0-56

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 (166) hide show
  1. package/CAPABILITIES.md +95 -0
  2. package/dist/packlets/ai-assist/cacheRequest.js +118 -0
  3. package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
  4. package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
  5. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  6. package/dist/packlets/ai-assist/completionClient.js +122 -45
  7. package/dist/packlets/ai-assist/completionClient.js.map +1 -1
  8. package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
  9. package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  10. package/dist/packlets/ai-assist/index.js +3 -1
  11. package/dist/packlets/ai-assist/index.js.map +1 -1
  12. package/dist/packlets/ai-assist/model.js.map +1 -1
  13. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
  14. package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  15. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
  16. package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  17. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
  18. package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  19. package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
  20. package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  21. package/dist/packlets/ai-assist/registry.js +9 -11
  22. package/dist/packlets/ai-assist/registry.js.map +1 -1
  23. package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
  24. package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  25. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
  26. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
  28. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
  30. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  31. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  32. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  33. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
  34. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  35. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
  36. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  37. package/dist/packlets/ai-assist/streamingClient.js +3 -2
  38. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  39. package/dist/packlets/ai-assist/structuredOutput.js +84 -7
  40. package/dist/packlets/ai-assist/structuredOutput.js.map +1 -1
  41. package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  42. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  43. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  44. package/dist/packlets/ai-assist/toolFormats.js +80 -0
  45. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  46. package/dist/packlets/ai-assist/toolTypes.js +21 -0
  47. package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
  48. package/dist/packlets/ai-assist/usageNormalization.js +178 -0
  49. package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
  50. package/dist/packlets/ai-assist/usageTypes.js +21 -0
  51. package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
  52. package/dist/packlets/mustache/index.js.map +1 -1
  53. package/dist/packlets/mustache/interfaces.js.map +1 -1
  54. package/dist/packlets/mustache/mustacheTemplate.js +91 -0
  55. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
  56. package/dist/ts-extras.d.ts +577 -30
  57. package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
  58. package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
  59. package/lib/packlets/ai-assist/cacheRequest.js +123 -0
  60. package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
  61. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
  62. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
  64. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  65. package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
  66. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/completionClient.js +121 -44
  68. package/lib/packlets/ai-assist/completionClient.js.map +1 -1
  69. package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
  70. package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
  71. package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
  72. package/lib/packlets/ai-assist/index.d.ts +7 -2
  73. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  74. package/lib/packlets/ai-assist/index.js +11 -1
  75. package/lib/packlets/ai-assist/index.js.map +1 -1
  76. package/lib/packlets/ai-assist/model.d.ts +59 -288
  77. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  78. package/lib/packlets/ai-assist/model.js.map +1 -1
  79. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
  80. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
  81. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
  82. package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
  83. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
  84. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
  85. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
  86. package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
  87. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
  88. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
  89. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
  90. package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
  91. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
  92. package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
  93. package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
  94. package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
  95. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/registry.js +9 -11
  97. package/lib/packlets/ai-assist/registry.js.map +1 -1
  98. package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
  99. package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
  100. package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
  101. package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
  102. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  103. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
  104. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  105. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
  106. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  107. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
  108. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  109. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
  110. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  111. package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
  112. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  113. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  114. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
  115. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  116. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
  117. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  118. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
  119. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  120. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
  121. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  122. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
  123. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  124. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  125. package/lib/packlets/ai-assist/streamingClient.js +3 -2
  126. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  127. package/lib/packlets/ai-assist/structuredOutput.d.ts +32 -0
  128. package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -1
  129. package/lib/packlets/ai-assist/structuredOutput.js +85 -7
  130. package/lib/packlets/ai-assist/structuredOutput.js.map +1 -1
  131. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +30 -0
  132. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -1
  133. package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
  134. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  135. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
  136. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  137. package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
  138. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  139. package/lib/packlets/ai-assist/toolFormats.js +82 -0
  140. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  141. package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
  142. package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
  143. package/lib/packlets/ai-assist/toolTypes.js +22 -0
  144. package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
  145. package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
  146. package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
  147. package/lib/packlets/ai-assist/usageNormalization.js +184 -0
  148. package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
  149. package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
  150. package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
  151. package/lib/packlets/ai-assist/usageTypes.js +22 -0
  152. package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
  153. package/lib/packlets/mustache/index.d.ts +1 -1
  154. package/lib/packlets/mustache/index.d.ts.map +1 -1
  155. package/lib/packlets/mustache/index.js.map +1 -1
  156. package/lib/packlets/mustache/interfaces.d.ts +30 -0
  157. package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
  158. package/lib/packlets/mustache/interfaces.js.map +1 -1
  159. package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
  160. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
  161. package/lib/packlets/mustache/mustacheTemplate.js +91 -0
  162. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
  163. package/package.json +16 -16
  164. package/perf/promptCacheAnthropicBreakpoint.js +135 -0
  165. package/perf/promptCacheObservability.js +107 -0
  166. package/perf/promptCacheRoutingAb.js +105 -0
@@ -1 +1 @@
1
- {"version":3,"file":"openaiResponses.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for the OpenAI / xAI Responses API. This is the format\n * used when server-side tools (e.g. web_search) are requested — Chat\n * Completions doesn't support tool progress events, but the Responses API\n * does.\n *\n * Client-defined tools (`function_call` type) are accumulated per call ID\n * and emitted as `client-tool-call-done` events when complete.\n *\n * @packageDocumentation\n */\n\nimport { type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport { buildMessages, buildOpenAiResponsesUserContent } from '../chatRequestBuilders';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toResponsesApiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport {\n IStreamApiConfig,\n UNRECOGNIZED_EVENT_WARN_TAG,\n formatUnrecognizedEventPayloadPreview,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * Accumulated state for a single function_call in the OpenAI Responses API stream.\n * @internal\n */\nexport interface IAccumulatedFunctionCall {\n readonly id: string;\n readonly name: string;\n argsBuffer: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * Payload of a `response.output_text.delta` SSE event.\n * @internal\n */\ninterface IResponsesDeltaPayload {\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.output_item.added` SSE event.\n * @internal\n */\ninterface IResponsesOutputItemAddedPayload {\n readonly item: {\n readonly type?: string;\n readonly id?: string;\n readonly name?: string;\n readonly call_id?: string;\n };\n}\n\n/**\n * Payload of a `response.function_call_arguments.delta` SSE event.\n *\n * The live OpenAI / xAI Responses API emits these events keyed by `item_id` (the\n * `fc_*` output-item id), NOT by `call_id` (the `call_*` id used in continuation\n * input items). The adapter correlates `item_id` → `call_id` via the\n * `response.output_item.added` event that introduced the function_call item.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDeltaPayload {\n readonly item_id: string;\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.function_call_arguments.done` SSE event.\n *\n * Keyed by `item_id` like the delta event — see {@link IResponsesFunctionCallArgsDeltaPayload}.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDonePayload {\n readonly item_id: string;\n readonly arguments: string;\n}\n\n/**\n * Payload of a `response.completed` SSE event.\n * @internal\n */\ninterface IResponsesCompletedPayload {\n readonly response: {\n readonly status?: string;\n readonly incomplete_details?: { readonly reason?: string };\n };\n}\n\n/**\n * Payload of a `response.failed` or `error` SSE event.\n * @internal\n */\ninterface IResponsesErrorPayload {\n readonly error?: { readonly message?: string };\n readonly message?: string;\n}\n\nconst responsesDeltaPayload: Validator<IResponsesDeltaPayload> = Validators.object<IResponsesDeltaPayload>({\n delta: Validators.string\n});\n\nconst responsesOutputItemInner: Validator<{\n type?: string;\n id?: string;\n name?: string;\n call_id?: string;\n}> = Validators.object<{ type?: string; id?: string; name?: string; call_id?: string }>(\n {\n type: Validators.string.optional(),\n id: Validators.string.optional(),\n name: Validators.string.optional(),\n call_id: Validators.string.optional()\n },\n { options: { optionalFields: ['type', 'id', 'name', 'call_id'] } }\n);\n\nconst responsesOutputItemAddedPayload: Validator<IResponsesOutputItemAddedPayload> =\n Validators.object<IResponsesOutputItemAddedPayload>({ item: responsesOutputItemInner });\n\nconst responsesFunctionCallArgsDeltaPayload: Validator<IResponsesFunctionCallArgsDeltaPayload> =\n Validators.object<IResponsesFunctionCallArgsDeltaPayload>({\n item_id: Validators.string,\n delta: Validators.string\n });\n\nconst responsesFunctionCallArgsDonePayload: Validator<IResponsesFunctionCallArgsDonePayload> =\n Validators.object<IResponsesFunctionCallArgsDonePayload>({\n item_id: Validators.string,\n arguments: Validators.string\n });\n\nconst responsesIncompleteDetails: Validator<{ reason?: string }> = Validators.object<{ reason?: string }>(\n { reason: Validators.string.optional() },\n { options: { optionalFields: ['reason'] } }\n);\n\nconst responsesCompletedPayload: Validator<IResponsesCompletedPayload> =\n Validators.object<IResponsesCompletedPayload>({\n response: Validators.object<{ status?: string; incomplete_details?: { reason?: string } }>(\n {\n status: Validators.string.optional(),\n incomplete_details: responsesIncompleteDetails.optional()\n },\n { options: { optionalFields: ['status', 'incomplete_details'] } }\n )\n });\n\nconst responsesErrorInner: Validator<{ message?: string }> = Validators.object<{ message?: string }>(\n { message: Validators.string.optional() },\n { options: { optionalFields: ['message'] } }\n);\n\nconst responsesErrorPayload: Validator<IResponsesErrorPayload> = Validators.object<IResponsesErrorPayload>(\n {\n error: responsesErrorInner.optional(),\n message: Validators.string.optional()\n },\n { options: { optionalFields: ['error', 'message'] } }\n);\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Recognized OpenAI / xAI Responses API SSE event names. An event in this set is either\n * handled below or intentionally ignored (lifecycle / reasoning content discarded by design /\n * server-tool channels not yet surfaced). Any event whose name is NOT in this set surfaces\n * a one-time `logger.warn` per stream — making provider drift (new event types from a model\n * update) visible empirically instead of silently no-op'd.\n *\n * Source: official `openai-node` SDK source (`src/resources/responses/responses.ts`)\n * event-type literal-string sweep, plus the xAI Responses superset. Add to this set when a\n * new event type is observed and confirmed safe to ignore.\n *\n * @internal\n */\nconst RECOGNIZED_OPENAI_RESPONSES_EVENTS: ReadonlySet<string> = new Set<string>([\n // ---- handled by the translator ----\n 'response.output_text.delta',\n 'response.web_search_call.in_progress',\n 'response.web_search_call.completed',\n 'response.output_item.added',\n 'response.function_call_arguments.delta',\n 'response.function_call_arguments.done',\n 'response.completed',\n 'response.failed',\n 'error',\n // ---- lifecycle events: intentionally silent ----\n 'response.created',\n 'response.in_progress',\n 'response.queued',\n 'response.incomplete',\n // ---- text content lifecycle: handled implicitly via delta accumulation ----\n 'response.output_text.done',\n 'response.output_text.annotation.added',\n 'response.content_part.added',\n 'response.content_part.done',\n 'response.output_item.done',\n // ---- reasoning content: discarded by design (see ai-assist-thinking-events follow-on) ----\n 'response.reasoning_summary_part.added',\n 'response.reasoning_summary_part.done',\n 'response.reasoning_summary_text.delta',\n 'response.reasoning_summary_text.done',\n 'response.reasoning_text.delta',\n 'response.reasoning_text.done',\n // ---- refusals: caller currently sees these via the model's final text ----\n 'response.refusal.delta',\n 'response.refusal.done',\n // ---- server-tool channels not surfaced as tool-event yet ----\n 'response.web_search_call.searching',\n 'response.file_search_call.in_progress',\n 'response.file_search_call.searching',\n 'response.file_search_call.completed',\n 'response.code_interpreter_call.in_progress',\n 'response.code_interpreter_call.interpreting',\n 'response.code_interpreter_call.completed',\n 'response.code_interpreter_call_code.delta',\n 'response.code_interpreter_call_code.done',\n 'response.image_generation_call.in_progress',\n 'response.image_generation_call.generating',\n 'response.image_generation_call.completed',\n 'response.image_generation_call.partial_image',\n 'response.mcp_call.in_progress',\n 'response.mcp_call.completed',\n 'response.mcp_call.failed',\n 'response.mcp_call_arguments.delta',\n 'response.mcp_call_arguments.done',\n 'response.mcp_list_tools.in_progress',\n 'response.mcp_list_tools.completed',\n 'response.mcp_list_tools.failed',\n 'response.custom_tool_call_input.delta',\n 'response.custom_tool_call_input.done',\n // ---- audio (not used in chat/tool flows here) ----\n 'response.audio.delta',\n 'response.audio.done',\n 'response.audio.transcript.delta',\n 'response.audio.transcript.done'\n]);\n\n/**\n * Translates an OpenAI Responses API SSE stream into unified events.\n *\n * Maintains a per-call-ID accumulation map for client-defined function calls.\n * The map is exposed via the passed-in `functionCallMap` parameter for the\n * C3 continuation builder to read after the stream completes.\n *\n * Unrecognized event names are reported once per stream via `logger?.warn` —\n * see {@link RECOGNIZED_OPENAI_RESPONSES_EVENTS}.\n *\n * @internal\n */\nasync function* translateOpenAiResponsesStream(\n response: Response,\n functionCallMap: Map<string, IAccumulatedFunctionCall>,\n logger?: Logging.ILogger\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let completed = false;\n let incompleteReason: string | undefined;\n // OpenAI / xAI Responses API emits function_call_arguments.{delta,done} events keyed by\n // `item_id` (the fc_* output-item id). The harness and continuation builder key by\n // `call_id` (the call_* id). This map correlates the two — populated when the\n // function_call item is announced in `response.output_item.added`.\n const itemIdToCallId = new Map<string, string>();\n // Track unrecognized event names we have already warned about, so a hot stream of\n // an unknown event type produces exactly one log line per name per stream.\n const warnedEvents = new Set<string>();\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const eventName = message.event;\n if (eventName === 'response.output_text.delta') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesDeltaPayload);\n /* c8 ignore next 1 - defensive: payload?.delta null branch unreachable after validation */\n const delta = payload?.delta;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n } else if (eventName === 'response.web_search_call.in_progress') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'started' };\n } else if (eventName === 'response.web_search_call.completed') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'completed' };\n } else if (eventName === 'response.output_item.added') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesOutputItemAddedPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const item = payload?.item;\n /* c8 ignore next 1 - defensive: falsy call_id/name branches are protocol violations */\n if (item?.type === 'function_call' && item.call_id && item.name) {\n functionCallMap.set(item.call_id, { id: item.call_id, name: item.name, argsBuffer: '' });\n // Reasoning models (and the standard flow) reference this function_call in subsequent\n // arguments.{delta,done} events by `item_id`, never by `call_id`. Record the mapping\n // so the arg-accumulation handlers can resolve the call.\n if (item.id) {\n itemIdToCallId.set(item.id, item.call_id);\n }\n yield { type: 'client-tool-call-start', toolName: item.name, callId: item.call_id };\n }\n } else if (eventName === 'response.function_call_arguments.delta') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDeltaPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan delta (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n /* c8 ignore next 1 - defensive: call always present and delta always string in valid stream */\n if (call && typeof payload?.delta === 'string') {\n call.argsBuffer += payload.delta;\n }\n }\n } else if (eventName === 'response.function_call_arguments.done') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDonePayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan done (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n if (call && callId !== undefined) {\n /* c8 ignore next 1 - defensive: payload?.arguments null branch unreachable after validation */\n const canonicalArgs = payload?.arguments ?? '{}';\n // Sync the accumulation entry with the canonical arguments from the .done event.\n // Delta events may carry partial/empty payloads; the .done event carries the\n // authoritative final arguments string used by the continuation builder.\n call.argsBuffer = canonicalArgs;\n let args: JsonObject;\n try {\n const parsed = JSON.parse(canonicalArgs) as unknown;\n /* c8 ignore start - defensive: non-object/malformed parse defaults to empty object */\n args =\n parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as JsonObject)\n : {};\n } catch {\n args = {};\n }\n /* c8 ignore stop */\n yield { type: 'client-tool-call-done', toolName: call.name, callId, args };\n }\n }\n } else if (eventName === 'response.completed') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesCompletedPayload);\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n if (payload) {\n truncated = payload.response.status === 'incomplete';\n // Per IAiStreamDone.incompleteReason's contract, the reason is meaningful only\n // when the response was truncated. Move both fields together on every completed\n // event so a stray incomplete_details on a non-incomplete payload never leaks\n // through, and a later (defensive) completed event can't leave a stale reason.\n incompleteReason = truncated ? payload.response.incomplete_details?.reason : undefined;\n }\n completed = true;\n /* c8 ignore next 1 - defensive: eventName === 'error' alternative not exercised in tests */\n } else if (eventName === 'response.failed' || eventName === 'error') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesErrorPayload);\n /* c8 ignore next 1 - defensive: payload?.error and payload?.message null branches unreachable */\n const errMsg = payload?.error?.message ?? payload?.message ?? 'Responses API stream failed';\n yield { type: 'error', message: errMsg };\n return;\n } else if (\n typeof eventName === 'string' &&\n !RECOGNIZED_OPENAI_RESPONSES_EVENTS.has(eventName) &&\n !warnedEvents.has(eventName)\n ) {\n // Empirical drift instrument: an unrecognized SSE event from the Responses API\n // surfaces as a one-time warning per stream. If a provider adds a new event type\n // (or changes an existing event's name), the warning fires the first time the\n // event arrives. Update RECOGNIZED_OPENAI_RESPONSES_EVENTS once the new event is\n // either handled or confirmed safe to ignore.\n warnedEvents.add(eventName);\n logger?.warn(\n `${UNRECOGNIZED_EVENT_WARN_TAG} OpenAI Responses adapter: unrecognized SSE event ` +\n `'${eventName}'. ` +\n `payload preview: ${formatUnrecognizedEventPayloadPreview(message.data)}. ` +\n `This may indicate provider drift — if the new event carries data the adapter ` +\n `should surface, add a handler; otherwise add the name to ` +\n `RECOGNIZED_OPENAI_RESPONSES_EVENTS.`\n );\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (completed) {\n yield { type: 'done', truncated, fullText, incompleteReason };\n } else {\n yield { type: 'error', message: 'Responses API stream ended without a completed event' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Responses API request (with tools) and returns the\n * unified-event iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiResponsesStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n tools: ReadonlyArray<AiToolConfig> = [],\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCallMap?: Map<string, IAccumulatedFunctionCall>,\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/responses`;\n const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n input,\n stream: true\n };\n // `tools` is omitted entirely when none are requested — a Responses-only model routed\n // here for tier/model reasons (not tools) must not send an empty tools array.\n if (tools.length > 0) {\n body.tools = toResponsesApiTools(tools);\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning = { effort };\n }\n // Temperature is sent only when the caller explicitly provided one (see callOpenAiChatStream).\n if (effort === undefined || effort === 'none') {\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n }\n // Shared by OpenAI and xAI — both route through the Responses API with the same field name.\n if (maxTokens !== undefined) {\n body.max_output_tokens = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n /* c8 ignore next 3 - optional logger */\n logger?.info(\n `OpenAI Responses streaming: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`\n );\n const callMap = functionCallMap ?? new Map<string, IAccumulatedFunctionCall>();\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) => succeed(translateOpenAiResponsesStream(response, callMap, logger)));\n}\n"]}
1
+ {"version":3,"file":"openaiResponses.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/openaiResponses.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming adapter for the OpenAI / xAI Responses API. This is the format\n * used when server-side tools (e.g. web_search) are requested — Chat\n * Completions doesn't support tool progress events, but the Responses API\n * does.\n *\n * Client-defined tools (`function_call` type) are accumulated per call ID\n * and emitted as `client-tool-call-done` events when complete.\n *\n * @packageDocumentation\n */\n\nimport { type Logging, Result, succeed, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonObject } from '@fgv/ts-json-base';\n\nimport { buildMessages, buildOpenAiResponsesUserContent } from '../chatRequestBuilders';\nimport { bearerAuthHeader } from '../endpoint';\nimport { AiPrompt, type AiToolConfig, type IAiStreamEvent, type IChatMessage } from '../model';\nimport { parseSseEventJson, readSseEvents } from '../sseParser';\nimport { toResponsesApiTools } from '../toolFormats';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { normalizeOpenAiResponsesUsage } from '../usageNormalization';\nimport {\n IStreamApiConfig,\n UNRECOGNIZED_EVENT_WARN_TAG,\n formatUnrecognizedEventPayloadPreview,\n jsonObjectOrNullValidator,\n openSseConnection,\n validateEventPayload\n} from './common';\n\n// ============================================================================\n// Accumulated call state (internal — used by C3 continuation builder)\n// ============================================================================\n\n/**\n * Accumulated state for a single function_call in the OpenAI Responses API stream.\n * @internal\n */\nexport interface IAccumulatedFunctionCall {\n readonly id: string;\n readonly name: string;\n argsBuffer: string;\n}\n\n// ============================================================================\n// Event payload shapes\n// ============================================================================\n\n/**\n * Payload of a `response.output_text.delta` SSE event.\n * @internal\n */\ninterface IResponsesDeltaPayload {\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.output_item.added` SSE event.\n * @internal\n */\ninterface IResponsesOutputItemAddedPayload {\n readonly item: {\n readonly type?: string;\n readonly id?: string;\n readonly name?: string;\n readonly call_id?: string;\n };\n}\n\n/**\n * Payload of a `response.function_call_arguments.delta` SSE event.\n *\n * The live OpenAI / xAI Responses API emits these events keyed by `item_id` (the\n * `fc_*` output-item id), NOT by `call_id` (the `call_*` id used in continuation\n * input items). The adapter correlates `item_id` → `call_id` via the\n * `response.output_item.added` event that introduced the function_call item.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDeltaPayload {\n readonly item_id: string;\n readonly delta: string;\n}\n\n/**\n * Payload of a `response.function_call_arguments.done` SSE event.\n *\n * Keyed by `item_id` like the delta event — see {@link IResponsesFunctionCallArgsDeltaPayload}.\n *\n * @internal\n */\ninterface IResponsesFunctionCallArgsDonePayload {\n readonly item_id: string;\n readonly arguments: string;\n}\n\n/**\n * Payload of a `response.completed` SSE event.\n * @internal\n */\ninterface IResponsesCompletedPayload {\n readonly response: {\n readonly status?: string;\n readonly incomplete_details?: { readonly reason?: string };\n /**\n * `null` when the provider has no usage block for this response (not just\n * absent) — must validate, or the whole `response.completed` payload is\n * rejected and `status`/`incomplete_details` are lost along with it.\n */\n // eslint-disable-next-line @rushstack/no-new-null\n readonly usage?: JsonObject | null;\n };\n}\n\n/**\n * Payload of a `response.failed` or `error` SSE event.\n * @internal\n */\ninterface IResponsesErrorPayload {\n readonly error?: { readonly message?: string };\n readonly message?: string;\n}\n\nconst responsesDeltaPayload: Validator<IResponsesDeltaPayload> = Validators.object<IResponsesDeltaPayload>({\n delta: Validators.string\n});\n\nconst responsesOutputItemInner: Validator<{\n type?: string;\n id?: string;\n name?: string;\n call_id?: string;\n}> = Validators.object<{ type?: string; id?: string; name?: string; call_id?: string }>(\n {\n type: Validators.string.optional(),\n id: Validators.string.optional(),\n name: Validators.string.optional(),\n call_id: Validators.string.optional()\n },\n { options: { optionalFields: ['type', 'id', 'name', 'call_id'] } }\n);\n\nconst responsesOutputItemAddedPayload: Validator<IResponsesOutputItemAddedPayload> =\n Validators.object<IResponsesOutputItemAddedPayload>({ item: responsesOutputItemInner });\n\nconst responsesFunctionCallArgsDeltaPayload: Validator<IResponsesFunctionCallArgsDeltaPayload> =\n Validators.object<IResponsesFunctionCallArgsDeltaPayload>({\n item_id: Validators.string,\n delta: Validators.string\n });\n\nconst responsesFunctionCallArgsDonePayload: Validator<IResponsesFunctionCallArgsDonePayload> =\n Validators.object<IResponsesFunctionCallArgsDonePayload>({\n item_id: Validators.string,\n arguments: Validators.string\n });\n\nconst responsesIncompleteDetails: Validator<{ reason?: string }> = Validators.object<{ reason?: string }>(\n { reason: Validators.string.optional() },\n { options: { optionalFields: ['reason'] } }\n);\n\nconst responsesCompletedPayload: Validator<IResponsesCompletedPayload> =\n Validators.object<IResponsesCompletedPayload>({\n response: Validators.object<{\n status?: string;\n incomplete_details?: { reason?: string };\n // eslint-disable-next-line @rushstack/no-new-null\n usage?: JsonObject | null;\n }>({\n status: Validators.string.optional(),\n incomplete_details: responsesIncompleteDetails.optional(),\n usage: jsonObjectOrNullValidator.optional()\n })\n });\n\nconst responsesErrorInner: Validator<{ message?: string }> = Validators.object<{ message?: string }>(\n { message: Validators.string.optional() },\n { options: { optionalFields: ['message'] } }\n);\n\nconst responsesErrorPayload: Validator<IResponsesErrorPayload> = Validators.object<IResponsesErrorPayload>(\n {\n error: responsesErrorInner.optional(),\n message: Validators.string.optional()\n },\n { options: { optionalFields: ['error', 'message'] } }\n);\n\n// ============================================================================\n// Stream translator\n// ============================================================================\n\n/**\n * Recognized OpenAI / xAI Responses API SSE event names. An event in this set is either\n * handled below or intentionally ignored (lifecycle / reasoning content discarded by design /\n * server-tool channels not yet surfaced). Any event whose name is NOT in this set surfaces\n * a one-time `logger.warn` per stream — making provider drift (new event types from a model\n * update) visible empirically instead of silently no-op'd.\n *\n * Source: official `openai-node` SDK source (`src/resources/responses/responses.ts`)\n * event-type literal-string sweep, plus the xAI Responses superset. Add to this set when a\n * new event type is observed and confirmed safe to ignore.\n *\n * @internal\n */\nconst RECOGNIZED_OPENAI_RESPONSES_EVENTS: ReadonlySet<string> = new Set<string>([\n // ---- handled by the translator ----\n 'response.output_text.delta',\n 'response.web_search_call.in_progress',\n 'response.web_search_call.completed',\n 'response.output_item.added',\n 'response.function_call_arguments.delta',\n 'response.function_call_arguments.done',\n 'response.completed',\n 'response.failed',\n 'error',\n // ---- lifecycle events: intentionally silent ----\n 'response.created',\n 'response.in_progress',\n 'response.queued',\n 'response.incomplete',\n // ---- text content lifecycle: handled implicitly via delta accumulation ----\n 'response.output_text.done',\n 'response.output_text.annotation.added',\n 'response.content_part.added',\n 'response.content_part.done',\n 'response.output_item.done',\n // ---- reasoning content: discarded by design (see ai-assist-thinking-events follow-on) ----\n 'response.reasoning_summary_part.added',\n 'response.reasoning_summary_part.done',\n 'response.reasoning_summary_text.delta',\n 'response.reasoning_summary_text.done',\n 'response.reasoning_text.delta',\n 'response.reasoning_text.done',\n // ---- refusals: caller currently sees these via the model's final text ----\n 'response.refusal.delta',\n 'response.refusal.done',\n // ---- server-tool channels not surfaced as tool-event yet ----\n 'response.web_search_call.searching',\n 'response.file_search_call.in_progress',\n 'response.file_search_call.searching',\n 'response.file_search_call.completed',\n 'response.code_interpreter_call.in_progress',\n 'response.code_interpreter_call.interpreting',\n 'response.code_interpreter_call.completed',\n 'response.code_interpreter_call_code.delta',\n 'response.code_interpreter_call_code.done',\n 'response.image_generation_call.in_progress',\n 'response.image_generation_call.generating',\n 'response.image_generation_call.completed',\n 'response.image_generation_call.partial_image',\n 'response.mcp_call.in_progress',\n 'response.mcp_call.completed',\n 'response.mcp_call.failed',\n 'response.mcp_call_arguments.delta',\n 'response.mcp_call_arguments.done',\n 'response.mcp_list_tools.in_progress',\n 'response.mcp_list_tools.completed',\n 'response.mcp_list_tools.failed',\n 'response.custom_tool_call_input.delta',\n 'response.custom_tool_call_input.done',\n // ---- audio (not used in chat/tool flows here) ----\n 'response.audio.delta',\n 'response.audio.done',\n 'response.audio.transcript.delta',\n 'response.audio.transcript.done'\n]);\n\n/**\n * Translates an OpenAI Responses API SSE stream into unified events.\n *\n * Maintains a per-call-ID accumulation map for client-defined function calls.\n * The map is exposed via the passed-in `functionCallMap` parameter for the\n * C3 continuation builder to read after the stream completes.\n *\n * Unrecognized event names are reported once per stream via `logger?.warn` —\n * see {@link RECOGNIZED_OPENAI_RESPONSES_EVENTS}.\n *\n * @internal\n */\nasync function* translateOpenAiResponsesStream(\n response: Response,\n functionCallMap: Map<string, IAccumulatedFunctionCall>,\n reportsUsage: boolean,\n logger?: Logging.ILogger\n): AsyncGenerator<IAiStreamEvent> {\n let fullText = '';\n let truncated = false;\n let completed = false;\n let incompleteReason: string | undefined;\n let usageRaw: JsonObject | undefined;\n // OpenAI / xAI Responses API emits function_call_arguments.{delta,done} events keyed by\n // `item_id` (the fc_* output-item id). The harness and continuation builder key by\n // `call_id` (the call_* id). This map correlates the two — populated when the\n // function_call item is announced in `response.output_item.added`.\n const itemIdToCallId = new Map<string, string>();\n // Track unrecognized event names we have already warned about, so a hot stream of\n // an unknown event type produces exactly one log line per name per stream.\n const warnedEvents = new Set<string>();\n\n try {\n /* c8 ignore next - body is non-null at this point per openSseConnection */\n if (!response.body) return;\n for await (const message of readSseEvents(response.body)) {\n const eventName = message.event;\n if (eventName === 'response.output_text.delta') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesDeltaPayload);\n /* c8 ignore next 1 - defensive: payload?.delta null branch unreachable after validation */\n const delta = payload?.delta;\n if (typeof delta === 'string' && delta.length > 0) {\n fullText += delta;\n yield { type: 'text-delta', delta };\n }\n } else if (eventName === 'response.web_search_call.in_progress') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'started' };\n } else if (eventName === 'response.web_search_call.completed') {\n yield { type: 'tool-event', toolType: 'web_search', phase: 'completed' };\n } else if (eventName === 'response.output_item.added') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesOutputItemAddedPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const item = payload?.item;\n /* c8 ignore next 1 - defensive: falsy call_id/name branches are protocol violations */\n if (item?.type === 'function_call' && item.call_id && item.name) {\n functionCallMap.set(item.call_id, { id: item.call_id, name: item.name, argsBuffer: '' });\n // Reasoning models (and the standard flow) reference this function_call in subsequent\n // arguments.{delta,done} events by `item_id`, never by `call_id`. Record the mapping\n // so the arg-accumulation handlers can resolve the call.\n if (item.id) {\n itemIdToCallId.set(item.id, item.call_id);\n }\n yield { type: 'client-tool-call-start', toolName: item.name, callId: item.call_id };\n }\n } else if (eventName === 'response.function_call_arguments.delta') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDeltaPayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan delta (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n /* c8 ignore next 1 - defensive: call always present and delta always string in valid stream */\n if (call && typeof payload?.delta === 'string') {\n call.argsBuffer += payload.delta;\n }\n }\n } else if (eventName === 'response.function_call_arguments.done') {\n const payload = validateEventPayload(\n parseSseEventJson(message.data),\n responsesFunctionCallArgsDonePayload\n );\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n const itemId = payload?.item_id;\n if (itemId !== undefined) {\n const callId = itemIdToCallId.get(itemId);\n /* c8 ignore next 1 - defensive: orphan done (no preceding output_item.added) is a protocol violation */\n const call = callId !== undefined ? functionCallMap.get(callId) : undefined;\n if (call && callId !== undefined) {\n /* c8 ignore next 1 - defensive: payload?.arguments null branch unreachable after validation */\n const canonicalArgs = payload?.arguments ?? '{}';\n // Sync the accumulation entry with the canonical arguments from the .done event.\n // Delta events may carry partial/empty payloads; the .done event carries the\n // authoritative final arguments string used by the continuation builder.\n call.argsBuffer = canonicalArgs;\n let args: JsonObject;\n try {\n const parsed = JSON.parse(canonicalArgs) as unknown;\n /* c8 ignore start - defensive: non-object/malformed parse defaults to empty object */\n args =\n parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as JsonObject)\n : {};\n } catch {\n args = {};\n }\n /* c8 ignore stop */\n yield { type: 'client-tool-call-done', toolName: call.name, callId, args };\n }\n }\n } else if (eventName === 'response.completed') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesCompletedPayload);\n /* c8 ignore next 1 - defensive: payload null branch unreachable after validation */\n if (payload) {\n truncated = payload.response.status === 'incomplete';\n // Per IAiStreamDone.incompleteReason's contract, the reason is meaningful only\n // when the response was truncated. Move both fields together on every completed\n // event so a stray incomplete_details on a non-incomplete payload never leaks\n // through, and a later (defensive) completed event can't leave a stale reason.\n incompleteReason = truncated ? payload.response.incomplete_details?.reason : undefined;\n // Gated the same way as the non-streaming Responses path (see\n // supportsCacheUsageReporting) — this adapter is shared by descriptors with no\n // confirmed cache-usage reporting.\n usageRaw = reportsUsage ? payload.response.usage ?? undefined : undefined;\n }\n completed = true;\n /* c8 ignore next 1 - defensive: eventName === 'error' alternative not exercised in tests */\n } else if (eventName === 'response.failed' || eventName === 'error') {\n const payload = validateEventPayload(parseSseEventJson(message.data), responsesErrorPayload);\n /* c8 ignore next 1 - defensive: payload?.error and payload?.message null branches unreachable */\n const errMsg = payload?.error?.message ?? payload?.message ?? 'Responses API stream failed';\n yield { type: 'error', message: errMsg };\n return;\n } else if (\n typeof eventName === 'string' &&\n !RECOGNIZED_OPENAI_RESPONSES_EVENTS.has(eventName) &&\n !warnedEvents.has(eventName)\n ) {\n // Empirical drift instrument: an unrecognized SSE event from the Responses API\n // surfaces as a one-time warning per stream. If a provider adds a new event type\n // (or changes an existing event's name), the warning fires the first time the\n // event arrives. Update RECOGNIZED_OPENAI_RESPONSES_EVENTS once the new event is\n // either handled or confirmed safe to ignore.\n warnedEvents.add(eventName);\n logger?.warn(\n `${UNRECOGNIZED_EVENT_WARN_TAG} OpenAI Responses adapter: unrecognized SSE event ` +\n `'${eventName}'. ` +\n `payload preview: ${formatUnrecognizedEventPayloadPreview(message.data)}. ` +\n `This may indicate provider drift — if the new event carries data the adapter ` +\n `should surface, add a handler; otherwise add the name to ` +\n `RECOGNIZED_OPENAI_RESPONSES_EVENTS.`\n );\n }\n }\n } catch (err: unknown) /* c8 ignore start - defensive: stream errors are always Error instances */ {\n yield { type: 'error', message: err instanceof Error ? err.message : String(err) };\n return;\n } /* c8 ignore stop */\n\n if (completed) {\n const usage = normalizeOpenAiResponsesUsage(usageRaw);\n yield { type: 'done', truncated, fullText, incompleteReason, ...(usage !== undefined ? { usage } : {}) };\n } else {\n yield { type: 'error', message: 'Responses API stream ended without a completed event' };\n }\n}\n\n// ============================================================================\n// Per-format request caller\n// ============================================================================\n\n/**\n * Issues a streaming Responses API request (with tools) and returns the\n * unified-event iterable on success.\n *\n * @internal\n */\nexport async function callOpenAiResponsesStream(\n config: IStreamApiConfig,\n prompt: AiPrompt,\n tools: ReadonlyArray<AiToolConfig> = [],\n messagesBefore: ReadonlyArray<IChatMessage> | undefined,\n temperature: number | undefined,\n logger?: Logging.ILogger,\n signal?: AbortSignal,\n resolvedThinking?: IResolvedThinkingConfig,\n functionCallMap?: Map<string, IAccumulatedFunctionCall>,\n continuationMessages?: ReadonlyArray<JsonObject>,\n maxTokens?: number,\n reportsUsage: boolean = false\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const url = `${config.baseUrl}/responses`;\n const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {\n head: messagesBefore,\n rawTail: continuationMessages\n });\n const effort = resolvedThinking?.openAiEffort ?? resolvedThinking?.xaiEffort;\n const supportsReasoning = config.model !== 'grok-4';\n const body: Record<string, unknown> = {\n model: config.model,\n input,\n stream: true\n };\n // `tools` is omitted entirely when none are requested — a Responses-only model routed\n // here for tier/model reasons (not tools) must not send an empty tools array.\n if (tools.length > 0) {\n body.tools = toResponsesApiTools(tools);\n }\n if (effort !== undefined && supportsReasoning) {\n body.reasoning = { effort };\n }\n // Temperature is sent only when the caller explicitly provided one (see callOpenAiChatStream).\n if (effort === undefined || effort === 'none') {\n if (temperature !== undefined) {\n body.temperature = temperature;\n }\n }\n // Shared by OpenAI and xAI — both route through the Responses API with the same field name.\n if (maxTokens !== undefined) {\n body.max_output_tokens = maxTokens;\n }\n if (resolvedThinking?.otherParams !== undefined) {\n Object.assign(body, resolvedThinking.otherParams);\n }\n const headers: Record<string, string> = bearerAuthHeader(config.apiKey);\n /* c8 ignore next 3 - optional logger */\n logger?.info(\n `OpenAI Responses streaming: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`\n );\n const callMap = functionCallMap ?? new Map<string, IAccumulatedFunctionCall>();\n const conn = await openSseConnection(url, headers, body, logger, signal);\n return conn.onSuccess((response) =>\n succeed(translateOpenAiResponsesStream(response, callMap, reportsUsage, logger))\n );\n}\n"]}
@@ -29,6 +29,7 @@ import { fail } from '@fgv/ts-utils';
29
29
  import { splitChatRequest } from './chatRequestBuilders';
30
30
  import { resolveEffectiveBaseUrl } from './endpoint';
31
31
  import { isAdaptiveThinkingModel, isResponsesOnlyModel, resolveProviderModel, usesMaxCompletionTokensField } from './model';
32
+ import { supportsCacheUsageReporting, supportsStreamUsageOption } from './streamUsageCapability';
32
33
  import { callAnthropicStream } from './streamingAdapters/anthropic';
33
34
  import { callGeminiStream } from './streamingAdapters/gemini';
34
35
  import { callOpenAiChatStream } from './streamingAdapters/openaiChat';
@@ -123,9 +124,9 @@ export async function callProviderCompletionStream(params) {
123
124
  // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
124
125
  // to the Responses stream even with no tools requested — same path the tools case uses.
125
126
  if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {
126
- return callOpenAiResponsesStream(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens);
127
+ return callOpenAiResponsesStream(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens, supportsCacheUsageReporting(descriptor));
127
128
  }
128
- return callOpenAiChatStream(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor));
129
+ return callOpenAiChatStream(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, usesMaxCompletionTokensField(descriptor), supportsStreamUsageOption(descriptor));
129
130
  case 'anthropic':
130
131
  return callAnthropicStream(config, prompt, head, temperature, tools, logger, signal, resolvedThinking, undefined, undefined, isAdaptiveThinkingModel(descriptor, config.model), maxTokens);
131
132
  case 'gemini':
@@ -1 +1 @@
1
- {"version":3,"file":"streamingClient.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming chat completion client. Public façade over the per-format\n * adapters under `streamingAdapters/`: provider dispatch and pre-flight\n * validation live here; format-specific request/translation logic and the\n * typed event-payload validators live with each adapter.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result } from '@fgv/ts-utils';\n\nimport { splitChatRequest } from './chatRequestBuilders';\nimport { resolveEffectiveBaseUrl } from './endpoint';\nimport {\n type IAiStreamEvent,\n type ModelSpecKey,\n isAdaptiveThinkingModel,\n isResponsesOnlyModel,\n resolveProviderModel,\n usesMaxCompletionTokensField\n} from './model';\nimport { callAnthropicStream } from './streamingAdapters/anthropic';\nimport { type IProviderCompletionStreamParams, type IStreamApiConfig } from './streamingAdapters/common';\nimport { callGeminiStream } from './streamingAdapters/gemini';\nimport { callOpenAiChatStream } from './streamingAdapters/openaiChat';\nimport { callOpenAiResponsesStream } from './streamingAdapters/openaiResponses';\nimport {\n checkTemperatureConflict,\n mergeThinkingConfig,\n providerDiscriminatorForId,\n type IResolvedThinkingConfig\n} from './thinkingOptionsResolver';\n\nexport { callProxiedCompletionStream } from './streamingAdapters/proxy';\nexport type { IProviderCompletionStreamParams } from './streamingAdapters/common';\n\n/**\n * Re-export `executeClientToolTurn` from the continuation builder so callers\n * can import it directly from the streaming-client surface.\n *\n * @remarks\n * **Why `clientTools` does not flow through `callProviderCompletionStream`:**\n * Client tools require a stateful per-stream accumulation buffer (for thinking\n * blocks, tool-use args, Gemini functionCall accumulation) that is owned by the\n * continuation builder's execution context. Passing `clientTools` through the\n * generic `callProviderCompletionStream` entry would require the caller to manage\n * the accumulation buffer externally. `executeClientToolTurn` is therefore the\n * canonical (and only) call path for client tools in Phase C. Callers that need\n * both server tools and client tools in the same request should use\n * `executeClientToolTurn` with both `tools` (server) and `clientTools` present;\n * `callProviderCompletionStream` is for server-tools-only or no-tools requests.\n */\n/* c8 ignore next 5 - barrel re-export; function is covered by clientToolContinuationBuilder tests */\nexport {\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingAdapters/clientToolContinuationBuilder';\n\n/**\n * Calls the appropriate streaming chat completion API for a given provider.\n *\n * @remarks\n * Pre-flight rejection: when `descriptor.streamingCorsRestricted === true`\n * and the call isn't being routed through a proxy, this returns\n * `Result.fail` before fetch is invoked. Callers should route through\n * {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.\n *\n * Connection-time failures (auth, network, non-2xx) surface as the outer\n * `Result.fail`. Once iteration begins, errors mid-stream surface as a\n * terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable\n * ending. The final successful event is {@link AiAssist.IAiStreamDone}.\n *\n * @param params - Request parameters including descriptor, API key, prompt, and optional tools\n * @returns A streaming iterable of unified events, or a Result.fail\n * @public\n */\nexport async function callProviderCompletionStream(\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n tier,\n logger,\n tools,\n signal,\n endpoint,\n thinking,\n maxTokens\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n if (descriptor.streamingCorsRestricted) {\n return fail(`provider \"${descriptor.id}\" requires a proxy for streaming; none is configured`);\n }\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const hasTools = tools !== undefined && tools.length > 0;\n const discriminator = providerDiscriminatorForId(descriptor.id);\n // The quality tier is the only completion-model selector; thinking and tools\n // are orthogonal request params/capabilities and never pick a model.\n const modelContext: ModelSpecKey | undefined = tier;\n\n const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);\n if (modelResult.isFailure()) {\n return fail(modelResult.message);\n }\n const model = modelResult.value;\n\n let resolvedThinking: IResolvedThinkingConfig | undefined;\n if (thinking !== undefined) {\n if (discriminator !== undefined) {\n const mergeResult = mergeThinkingConfig(thinking, model, discriminator);\n /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */\n if (mergeResult.isFailure()) {\n return fail(mergeResult.message);\n }\n resolvedThinking = mergeResult.value;\n const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);\n if (conflictResult.isFailure()) {\n return fail(conflictResult.message);\n }\n }\n }\n\n const config: IStreamApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model\n };\n\n switch (descriptor.apiFormat) {\n case 'openai':\n // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route\n // to the Responses stream even with no tools requested — same path the tools case uses.\n if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {\n return callOpenAiResponsesStream(\n config,\n prompt,\n tools,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n }\n return callOpenAiChatStream(\n config,\n prompt,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens,\n usesMaxCompletionTokensField(descriptor)\n );\n case 'anthropic':\n return callAnthropicStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n isAdaptiveThinkingModel(descriptor, config.model),\n maxTokens\n );\n case 'gemini':\n return callGeminiStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return fail(`unsupported API format: ${String(_exhaustive)}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"streamingClient.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/streamingClient.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Streaming chat completion client. Public façade over the per-format\n * adapters under `streamingAdapters/`: provider dispatch and pre-flight\n * validation live here; format-specific request/translation logic and the\n * typed event-payload validators live with each adapter.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result } from '@fgv/ts-utils';\n\nimport { splitChatRequest } from './chatRequestBuilders';\nimport { resolveEffectiveBaseUrl } from './endpoint';\nimport {\n type IAiStreamEvent,\n type ModelSpecKey,\n isAdaptiveThinkingModel,\n isResponsesOnlyModel,\n resolveProviderModel,\n usesMaxCompletionTokensField\n} from './model';\nimport { supportsCacheUsageReporting, supportsStreamUsageOption } from './streamUsageCapability';\nimport { callAnthropicStream } from './streamingAdapters/anthropic';\nimport { type IProviderCompletionStreamParams, type IStreamApiConfig } from './streamingAdapters/common';\nimport { callGeminiStream } from './streamingAdapters/gemini';\nimport { callOpenAiChatStream } from './streamingAdapters/openaiChat';\nimport { callOpenAiResponsesStream } from './streamingAdapters/openaiResponses';\nimport {\n checkTemperatureConflict,\n mergeThinkingConfig,\n providerDiscriminatorForId,\n type IResolvedThinkingConfig\n} from './thinkingOptionsResolver';\n\nexport { callProxiedCompletionStream } from './streamingAdapters/proxy';\nexport type { IProviderCompletionStreamParams } from './streamingAdapters/common';\n\n/**\n * Re-export `executeClientToolTurn` from the continuation builder so callers\n * can import it directly from the streaming-client surface.\n *\n * @remarks\n * **Why `clientTools` does not flow through `callProviderCompletionStream`:**\n * Client tools require a stateful per-stream accumulation buffer (for thinking\n * blocks, tool-use args, Gemini functionCall accumulation) that is owned by the\n * continuation builder's execution context. Passing `clientTools` through the\n * generic `callProviderCompletionStream` entry would require the caller to manage\n * the accumulation buffer externally. `executeClientToolTurn` is therefore the\n * canonical (and only) call path for client tools in Phase C. Callers that need\n * both server tools and client tools in the same request should use\n * `executeClientToolTurn` with both `tools` (server) and `clientTools` present;\n * `callProviderCompletionStream` is for server-tools-only or no-tools requests.\n */\n/* c8 ignore next 5 - barrel re-export; function is covered by clientToolContinuationBuilder tests */\nexport {\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingAdapters/clientToolContinuationBuilder';\n\n/**\n * Calls the appropriate streaming chat completion API for a given provider.\n *\n * @remarks\n * Pre-flight rejection: when `descriptor.streamingCorsRestricted === true`\n * and the call isn't being routed through a proxy, this returns\n * `Result.fail` before fetch is invoked. Callers should route through\n * {@link AiAssist.callProxiedCompletionStream} or surface the failure to the user.\n *\n * Connection-time failures (auth, network, non-2xx) surface as the outer\n * `Result.fail`. Once iteration begins, errors mid-stream surface as a\n * terminal error event ({@link AiAssist.IAiStreamError}) followed by the iterable\n * ending. The final successful event is {@link AiAssist.IAiStreamDone}.\n *\n * @param params - Request parameters including descriptor, API key, prompt, and optional tools\n * @returns A streaming iterable of unified events, or a Result.fail\n * @public\n */\nexport async function callProviderCompletionStream(\n params: IProviderCompletionStreamParams\n): Promise<Result<AsyncIterable<IAiStreamEvent>>> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n temperature,\n modelOverride,\n tier,\n logger,\n tools,\n signal,\n endpoint,\n thinking,\n maxTokens\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n if (descriptor.streamingCorsRestricted) {\n return fail(`provider \"${descriptor.id}\" requires a proxy for streaming; none is configured`);\n }\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n const hasTools = tools !== undefined && tools.length > 0;\n const discriminator = providerDiscriminatorForId(descriptor.id);\n // The quality tier is the only completion-model selector; thinking and tools\n // are orthogonal request params/capabilities and never pick a model.\n const modelContext: ModelSpecKey | undefined = tier;\n\n const modelResult = resolveProviderModel(descriptor, modelOverride, modelContext);\n if (modelResult.isFailure()) {\n return fail(modelResult.message);\n }\n const model = modelResult.value;\n\n let resolvedThinking: IResolvedThinkingConfig | undefined;\n if (thinking !== undefined) {\n if (discriminator !== undefined) {\n const mergeResult = mergeThinkingConfig(thinking, model, discriminator);\n /* c8 ignore next 3 - mergeThinkingConfig always succeeds; defensive guard */\n if (mergeResult.isFailure()) {\n return fail(mergeResult.message);\n }\n resolvedThinking = mergeResult.value;\n const conflictResult = checkTemperatureConflict(resolvedThinking, discriminator, temperature);\n if (conflictResult.isFailure()) {\n return fail(conflictResult.message);\n }\n }\n }\n\n const config: IStreamApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model\n };\n\n switch (descriptor.apiFormat) {\n case 'openai':\n // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route\n // to the Responses stream even with no tools requested — same path the tools case uses.\n if (hasTools || isResponsesOnlyModel(descriptor, config.model)) {\n return callOpenAiResponsesStream(\n config,\n prompt,\n tools,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens,\n supportsCacheUsageReporting(descriptor)\n );\n }\n return callOpenAiChatStream(\n config,\n prompt,\n head,\n temperature,\n logger,\n signal,\n resolvedThinking,\n maxTokens,\n usesMaxCompletionTokensField(descriptor),\n supportsStreamUsageOption(descriptor)\n );\n case 'anthropic':\n return callAnthropicStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n isAdaptiveThinkingModel(descriptor, config.model),\n maxTokens\n );\n case 'gemini':\n return callGeminiStream(\n config,\n prompt,\n head,\n temperature,\n tools,\n logger,\n signal,\n resolvedThinking,\n undefined,\n undefined,\n maxTokens\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return fail(`unsupported API format: ${String(_exhaustive)}`);\n }\n }\n}\n"]}
@@ -130,6 +130,17 @@ function jsonObjectWire(format) {
130
130
  * So this is treated as a **capability mismatch** and routed through the caller's
131
131
  * existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly
132
132
  * on request. Gemini and Anthropic have no such rule and are unaffected.
133
+ *
134
+ * **One narrow exception, and it does not weaken the above.** The first repair is
135
+ * unsafe *because the rewritten schema admits a reply the original rejects*. When
136
+ * the optional property's node **already admits `null`**, as it does when authored
137
+ * `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so
138
+ * listing the key in `required` only removes the model's option to omit it, and
139
+ * every reply the emitted schema permits still satisfies the supplied one. That
140
+ * case is hoisted by {@link hoistNullableOptionals} when the caller opts in via
141
+ * `adaptOptionalToNullable`, **and this function is then re-run on the result** —
142
+ * so a property that is genuinely not `null`-able still lands here and still
143
+ * refuses. The condition is read off the schema, never asserted by the caller.
133
144
  * @internal
134
145
  */
135
146
  export function hasOptionalProperties(raw) {
@@ -139,17 +150,68 @@ export function hasOptionalProperties(raw) {
139
150
  if (raw === null || typeof raw !== 'object') {
140
151
  return false;
141
152
  }
142
- const node = raw;
143
- const properties = node.properties;
153
+ const properties = raw.properties;
144
154
  if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
145
- const required = Array.isArray(node.required) ? node.required : [];
155
+ const required = Array.isArray(raw.required) ? raw.required : [];
146
156
  for (const name of Object.keys(properties)) {
147
157
  if (!required.includes(name)) {
148
158
  return true;
149
159
  }
150
160
  }
151
161
  }
152
- return Object.values(node).some((v) => v !== undefined && hasOptionalProperties(v));
162
+ return Object.values(raw).some(hasOptionalProperties);
163
+ }
164
+ /** Whether a wire node's `type` admits `null` — either spelling. @internal */
165
+ function admitsNull(node) {
166
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) {
167
+ return false;
168
+ }
169
+ return Array.isArray(node.type) && node.type.includes('null');
170
+ }
171
+ /**
172
+ * Rewrites `raw` so that every optional property whose node already admits `null`
173
+ * is listed in its parent's `required` array, at any depth.
174
+ *
175
+ * @remarks
176
+ * The rewrite is deliberately **narrow, and its narrowness is the safety argument.**
177
+ * `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored
178
+ * as `optional(string({ nullable: true }))` is already `['string', 'null']` on the
179
+ * wire and differs from its required sibling only by absence from `required`.
180
+ * Adding it there narrows the permitted replies from *absent-or-null-or-value* to
181
+ * *null-or-value* — a strict subset of what the caller's own schema accepts. No
182
+ * reply that satisfies the emitted schema can fail the supplied one.
183
+ *
184
+ * A property whose node does not admit `null` is left exactly as it was, which is
185
+ * what makes this composable with the existing guard rather than a replacement for
186
+ * it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable
187
+ * optional still routes through `onUnsupported`. **The verification is the original
188
+ * check, applied again** — there is no second notion of correctness to keep in sync.
189
+ * @internal
190
+ */
191
+ export function hoistNullableOptionals(raw) {
192
+ if (Array.isArray(raw)) {
193
+ return raw.map(hoistNullableOptionals);
194
+ }
195
+ if (raw === null || typeof raw !== 'object') {
196
+ return raw;
197
+ }
198
+ const out = {};
199
+ for (const [key, value] of Object.entries(raw)) {
200
+ out[key] = hoistNullableOptionals(value);
201
+ }
202
+ const properties = out.properties;
203
+ if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
204
+ const required = Array.isArray(out.required) ? [...out.required] : [];
205
+ for (const [name, propSchema] of Object.entries(properties)) {
206
+ if (!required.includes(name) && admitsNull(propSchema)) {
207
+ required.push(name);
208
+ }
209
+ }
210
+ if (required.length > 0) {
211
+ out.required = required;
212
+ }
213
+ }
214
+ return out;
153
215
  }
154
216
  /** The two formats that carry OpenAI's all-properties-required strict rule. @internal */
155
217
  function isOpenAiStrictFormat(format) {
@@ -237,13 +299,28 @@ export function resolveStructuredOutput(descriptor, model, request, serverTools,
237
299
  let resolved;
238
300
  let unsupported;
239
301
  if (request.mode === 'schema') {
240
- const raw = request.schema.toJson();
241
- if (isOpenAiStrictFormat(format) && hasOptionalProperties(raw)) {
302
+ // Hoist BEFORE the guard, then let the guard judge the result. The rewrite only
303
+ // ever removes optionality that was safe to remove, so re-running the original
304
+ // check is the whole verification — a schema that still trips it was not
305
+ // adaptable, and refuses exactly as it did before the flag existed.
306
+ // Gated on the format, not just the flag: hoisting narrows what the model may
307
+ // send, so applying it where the all-required rule does not exist would change
308
+ // a reply on a provider that never needed it changed.
309
+ const strict = isOpenAiStrictFormat(format);
310
+ const adapt = strict && request.adaptOptionalToNullable === true;
311
+ const raw = adapt ? hoistNullableOptionals(request.schema.toJson()) : request.schema.toJson();
312
+ if (strict && hasOptionalProperties(raw)) {
242
313
  // See `hasOptionalProperties` — a hard provider constraint, treated as a
243
314
  // capability mismatch rather than relocated into an opaque 400.
244
315
  unsupported =
245
316
  `the supplied schema declares optional properties, and OpenAI strict structured output ` +
246
- `requires every property to be required; author them as required, or pass ` +
317
+ `requires every property to be required; ` +
318
+ (adapt
319
+ ? `adaptOptionalToNullable hoisted the ones that admit null, but at least one does not — ` +
320
+ `author it as nullable (e.g. optional(string({ nullable: true }))) so null is an ` +
321
+ `accepted reply, make it required, or pass `
322
+ : `author them as required, adopt adaptOptionalToNullable if null is an accepted reply ` +
323
+ `for each of them, or pass `) +
247
324
  `onUnsupported: 'degrade' to send the request unconstrained`;
248
325
  }
249
326
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type { JsonObject, JsonValue } from '@fgv/ts-json-base';\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport type { AiServerToolConfig, IAiProviderDescriptor } from './model';\nimport type {\n IAiStructuredOutputCapability,\n StructuredOutputEnforcement,\n StructuredOutputFallback,\n StructuredOutputRequest\n} from './structuredOutputTypes';\nimport { toGeminiParameterSchema } from './toolFormats';\n\n/**\n * The name the Anthropic forced-tool path gives its synthetic tool.\n *\n * @remarks\n * Anthropic has no `response_format`; its structured-output mechanism is forced\n * tool use, so a tool must exist to be forced. The name is fgv-owned and never\n * reaches the caller — the structured-output resolver re-serializes the tool's\n * `input` back into `IAiCompletionResponse.content`, so a caller's converter sees\n * a JSON string exactly as it does on every other provider.\n * @public\n */\nexport const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME: string = 'fgv_structured_output';\n\n/**\n * A resolved structured-output decision: what will be enforced, and the wire\n * fields that enforce it.\n * @internal\n */\nexport interface IResolvedStructuredOutput {\n /** What to report on the response. */\n readonly enforcement: StructuredOutputEnforcement;\n /**\n * Fields to merge into the request, **at the location the format dictates** —\n * the request body for the OpenAI and Anthropic formats, `generationConfig` for\n * Gemini. Empty when `enforcement` is `'none'`.\n */\n readonly wire: JsonObject;\n}\n\n/** The `'none'` decision: nothing sent, nothing enforced. @internal */\nexport const NO_STRUCTURED_OUTPUT: IResolvedStructuredOutput = { enforcement: 'none', wire: {} };\n\n/**\n * Wire fields for a schema-constrained request. Every format can express this —\n * a structured-output capability that could not carry a schema would have nothing\n * to declare.\n * @internal\n */\nfunction schemaWire(\n format: IAiStructuredOutputCapability['format'],\n raw: JsonValue\n): IResolvedStructuredOutput {\n switch (format) {\n case 'openai-json-schema':\n return {\n enforcement: 'schema',\n wire: {\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'response', strict: true, schema: raw }\n }\n }\n };\n case 'openai-responses-format':\n // The Responses API nests the same choice under `text.format` and flattens the\n // schema onto the format object rather than a `json_schema` sub-object.\n return {\n enforcement: 'schema',\n wire: { text: { format: { type: 'json_schema', name: 'response', strict: true, schema: raw } } }\n };\n case 'gemini-response-schema':\n // Merged into `generationConfig`, not the body. Gemini's schema is an\n // OpenAPI-3.0 subset that REJECTS draft-07 keywords rather than ignoring them,\n // and `JsonSchema` is strict-by-default so `.toJson()` emits\n // `additionalProperties: false` on every object node — hence the same sanitizer\n // the tool path uses.\n return {\n enforcement: 'schema',\n wire: { responseMimeType: 'application/json', responseSchema: toGeminiParameterSchema(raw) }\n };\n case 'anthropic-tool-forced':\n // Anthropic has no response-format field. The schema becomes a synthetic\n // tool's `input_schema` and `tool_choice` forces it, which is why this is a\n // distinct enforcement value rather than a spelling of `'schema'`: the reply\n // arrives in a `tool_use` block, not as text.\n return {\n enforcement: 'tool-forced',\n wire: {\n tools: [\n {\n name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,\n description: 'Return the response as structured data matching the supplied schema.',\n input_schema: raw\n }\n ],\n tool_choice: { type: 'tool', name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME }\n }\n };\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Wire fields for a bare JSON-object request, or `undefined` when the format\n * cannot express one.\n *\n * @remarks\n * The `undefined` return **is** the capability table — there is deliberately no\n * separate `supportsJsonObject` flag anywhere, because a second declaration of\n * what a format can do could only ever disagree with this function.\n * @internal\n */\nfunction jsonObjectWire(\n format: IAiStructuredOutputCapability['format']\n): IResolvedStructuredOutput | undefined {\n switch (format) {\n case 'openai-json-schema':\n return { enforcement: 'json-mode', wire: { response_format: { type: 'json_object' } } };\n case 'openai-responses-format':\n return { enforcement: 'json-mode', wire: { text: { format: { type: 'json_object' } } } };\n case 'gemini-response-schema':\n return { enforcement: 'json-mode', wire: { responseMimeType: 'application/json' } };\n case 'anthropic-tool-forced':\n // A forced tool needs an input schema to be forced *to*, so there is no\n // schema-less form of this mechanism.\n return undefined;\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Whether `raw` declares any object property that is absent from that object's\n * `required` list — at any depth.\n *\n * @remarks\n * **This is a hard constraint of OpenAI's strict structured output, not a style\n * preference.** `response_format: { type: 'json_schema', json_schema: { strict: true } }`\n * requires *every* key in `properties` to appear in `required`; a schema that omits\n * one is rejected with a 400 before the model ever runs. `JsonSchema.optional(...)`\n * produces exactly that shape, so an authored schema with one optional field is\n * unsendable to the two OpenAI strict formats.\n *\n * The three obvious repairs are all worse than refusing. Rewriting optional to\n * required-and-nullable changes what the model must emit (`null` rather than\n * omission), so the reply would no longer satisfy the caller's own validator —\n * breaking the one-object-cannot-drift property this whole surface exists for.\n * Dropping `strict` silently downgrades the guarantee while still reporting\n * `'schema'`, which is the lie the required report exists to prevent. And sending\n * it anyway just relocates the failure to an opaque provider 400.\n *\n * So this is treated as a **capability mismatch** and routed through the caller's\n * existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly\n * on request. Gemini and Anthropic have no such rule and are unaffected.\n * @internal\n */\nexport function hasOptionalProperties(raw: JsonValue): boolean {\n if (Array.isArray(raw)) {\n return raw.some(hasOptionalProperties);\n }\n if (raw === null || typeof raw !== 'object') {\n return false;\n }\n const node: Record<string, JsonValue | undefined> = raw as Record<string, JsonValue | undefined>;\n const properties = node.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: ReadonlyArray<JsonValue> = Array.isArray(node.required) ? node.required : [];\n for (const name of Object.keys(properties)) {\n if (!required.includes(name)) {\n return true;\n }\n }\n }\n return Object.values(node).some((v) => v !== undefined && hasOptionalProperties(v));\n}\n\n/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */\nfunction isOpenAiStrictFormat(format: IAiStructuredOutputCapability['format']): boolean {\n return format === 'openai-json-schema' || format === 'openai-responses-format';\n}\n\n/**\n * Whether a resolved wire claims the provider's tools channel, and therefore\n * genuinely conflicts with server-side tools.\n *\n * @remarks\n * Asked of the **resolved wire** rather than the declared format, because a format\n * that *would* claim the channel does not claim it when the request degraded to\n * sending nothing. Anthropic + `json-object` is exactly that case: the mode has no\n * expression there, so the wire is empty and there is nothing to conflict with —\n * rejecting it would refuse a request that was about to become harmless.\n * @internal\n */\nfunction conflictsWithServerTools(\n format: IAiStructuredOutputCapability['format'],\n resolved: IResolvedStructuredOutput\n): boolean {\n return (\n resolved.enforcement !== 'none' &&\n (format === 'anthropic-tool-forced' || format === 'gemini-response-schema')\n );\n}\n\n/**\n * The wire format actually in force, given which OpenAI endpoint the dispatcher\n * will use.\n *\n * @remarks\n * **The OpenAI route is not a function of the model alone.** `callProviderCompletion`\n * sends a request to `/responses` when it carries server tools **or** when the model\n * is Responses-only, and to `/chat/completions` otherwise — so the same model takes\n * different endpoints on different calls, and those endpoints spell structured output\n * differently (`response_format` vs `text.format`). A capability declaration keyed on\n * the model therefore cannot name the right one by itself, and emitting\n * `response_format` into a `/responses` body would be silently ignored by the\n * provider: the request would look constrained and the reply would not be, with the\n * report confidently saying `'schema'`.\n *\n * The declaration still names each family's *support*; this is the one axis it cannot\n * carry, so it is supplied by the dispatcher that makes the routing decision.\n * @internal\n */\nfunction effectiveFormat(\n declared: IAiStructuredOutputCapability['format'],\n usesResponsesApi: boolean\n): IAiStructuredOutputCapability['format'] {\n if (usesResponsesApi && declared === 'openai-json-schema') {\n return 'openai-responses-format';\n }\n return declared;\n}\n\n/**\n * Resolve a caller's structured-output request against the concrete model that\n * will serve it.\n *\n * @param descriptor - The provider descriptor.\n * @param model - The **concrete** model id, already through `resolveProviderModel`.\n * Passing an alias here would be a bug of the class `resolveImageCapability` once\n * had, where an unresolved alias fell through to a catch-all `modelPrefix: ''` and\n * returned a confidently wrong capability.\n * @param request - The caller's intent, or `undefined` for no request at all.\n * @param serverTools - Server-side tools on the same request, which conflict with\n * structured output on two of the four formats.\n * @param usesResponsesApi - Whether the dispatcher will send this request to the\n * OpenAI Responses API rather than Chat Completions. See {@link effectiveFormat} —\n * the route is not a function of the model alone, so the capability declaration\n * cannot carry it.\n * @returns The decision, or `Failure` when the caller asked to fail rather than\n * degrade — or when the request conflicts with server tools, which is never\n * degradable because the caller asked for two things the provider cannot both do.\n * @internal\n */\nexport function resolveStructuredOutput(\n descriptor: IAiProviderDescriptor,\n model: string,\n request: StructuredOutputRequest | undefined,\n serverTools: ReadonlyArray<AiServerToolConfig> | undefined,\n usesResponsesApi: boolean,\n resolveCapability: (\n descriptor: IAiProviderDescriptor,\n model: string\n ) => IAiStructuredOutputCapability | undefined\n): Result<IResolvedStructuredOutput> {\n if (request === undefined) {\n return succeed(NO_STRUCTURED_OUTPUT);\n }\n const fallback: StructuredOutputFallback = request.onUnsupported ?? 'degrade';\n const capability = resolveCapability(descriptor, model);\n if (capability === undefined) {\n return fallback === 'fail'\n ? fail(\n `provider '${descriptor.id}' model '${model}' declares no structured-output capability; ` +\n `pass onUnsupported: 'degrade' to send the request unconstrained`\n )\n : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n const format = effectiveFormat(capability.format, usesResponsesApi);\n\n // Resolve the wire FIRST, then judge conflicts against what it actually is.\n // Ordering matters: a format that would claim the tools channel does not claim\n // it when the request degraded to sending nothing.\n let resolved: IResolvedStructuredOutput | undefined;\n let unsupported: string | undefined;\n if (request.mode === 'schema') {\n const raw: JsonValue = request.schema.toJson();\n if (isOpenAiStrictFormat(format) && hasOptionalProperties(raw)) {\n // See `hasOptionalProperties` — a hard provider constraint, treated as a\n // capability mismatch rather than relocated into an opaque 400.\n unsupported =\n `the supplied schema declares optional properties, and OpenAI strict structured output ` +\n `requires every property to be required; author them as required, or pass ` +\n `onUnsupported: 'degrade' to send the request unconstrained`;\n } else {\n resolved = schemaWire(format, raw);\n }\n } else {\n resolved = jsonObjectWire(format);\n if (resolved === undefined) {\n // Today this is only `'json-object'` on Anthropic, whose mechanism needs a\n // schema to force a tool to.\n unsupported = `provider '${descriptor.id}' model '${model}' cannot enforce '${request.mode}' structured output`;\n }\n }\n\n if (resolved === undefined) {\n return fallback === 'fail' ? fail(`${unsupported}`) : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n // Two formats cannot carry structured output and server-side tools at once, for\n // DIFFERENT reasons — worth separating, because a reader who assumes one\n // mechanism will reason wrongly about the other.\n //\n // anthropic-tool-forced: a wire-level clash. The constraint IS `tools` +\n // `tool_choice`, so server tools would be overwritten (and `tool_choice`\n // forces ours, which disables theirs anyway).\n // gemini-response-schema: NOT a wire clash — `responseMimeType` /\n // `responseSchema` live in `generationConfig`, nowhere near `tools`. It is\n // an API-level mutual exclusivity Gemini enforces, the same restriction the\n // client-tool path already pre-empts.\n //\n // Neither is degradable: silently dropping either half would give the caller\n // something they did not ask for, and `onUnsupported` speaks to what a model can\n // enforce, not to a caller asking for two incompatible things.\n if (serverTools !== undefined && serverTools.length > 0 && conflictsWithServerTools(format, resolved)) {\n const why =\n format === 'anthropic-tool-forced'\n ? 'Anthropic enforces structured output by forcing a tool, so it cannot be combined with'\n : 'Gemini cannot combine a response schema with';\n return fail(\n `${why} server-side tools (${serverTools.map((t) => t.type).join(', ')}) in the same request; ` +\n `send one or the other`\n );\n }\n\n return succeed(resolved);\n}\n\n/**\n * Every valid `StructuredOutputEnforcement`, for the wire-shape guard below.\n *\n * @remarks\n * A **total** `Record`, not a `Set` built from an array literal — the same reasoning\n * as `SCHEMA_NODE_TYPES` in `@fgv/ts-json-base`. A `Set` catches a removed or\n * misspelled member but not an *added* one, so a new enforcement value would compile\n * fine here while this guard silently began rejecting it off a proxy response. The\n * `Record` makes that addition a compile error at this line.\n * @internal\n */\nconst ENFORCEMENTS: Readonly<Record<StructuredOutputEnforcement, true>> = {\n none: true,\n 'json-mode': true,\n schema: true,\n 'tool-forced': true\n};\n\n/**\n * Whether an untyped value off a proxy response is a valid\n * `StructuredOutputEnforcement`.\n * @internal\n */\nexport function isStructuredOutputEnforcement(value: unknown): value is StructuredOutputEnforcement {\n // Indexed read compared to `true`, NOT `in` — `in` walks the prototype chain, so a\n // proxy answering `structuredOutput: 'constructor'` would pass it.\n return typeof value === 'string' && ENFORCEMENTS[value as StructuredOutputEnforcement] === true;\n}\n"]}
1
+ {"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type { JsonObject, JsonValue } from '@fgv/ts-json-base';\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport type { AiServerToolConfig, IAiProviderDescriptor } from './model';\nimport type {\n IAiStructuredOutputCapability,\n StructuredOutputEnforcement,\n StructuredOutputFallback,\n StructuredOutputRequest\n} from './structuredOutputTypes';\nimport { toGeminiParameterSchema } from './toolFormats';\n\n/**\n * The name the Anthropic forced-tool path gives its synthetic tool.\n *\n * @remarks\n * Anthropic has no `response_format`; its structured-output mechanism is forced\n * tool use, so a tool must exist to be forced. The name is fgv-owned and never\n * reaches the caller — the structured-output resolver re-serializes the tool's\n * `input` back into `IAiCompletionResponse.content`, so a caller's converter sees\n * a JSON string exactly as it does on every other provider.\n * @public\n */\nexport const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME: string = 'fgv_structured_output';\n\n/**\n * A resolved structured-output decision: what will be enforced, and the wire\n * fields that enforce it.\n * @internal\n */\nexport interface IResolvedStructuredOutput {\n /** What to report on the response. */\n readonly enforcement: StructuredOutputEnforcement;\n /**\n * Fields to merge into the request, **at the location the format dictates** —\n * the request body for the OpenAI and Anthropic formats, `generationConfig` for\n * Gemini. Empty when `enforcement` is `'none'`.\n */\n readonly wire: JsonObject;\n}\n\n/** The `'none'` decision: nothing sent, nothing enforced. @internal */\nexport const NO_STRUCTURED_OUTPUT: IResolvedStructuredOutput = { enforcement: 'none', wire: {} };\n\n/**\n * Wire fields for a schema-constrained request. Every format can express this —\n * a structured-output capability that could not carry a schema would have nothing\n * to declare.\n * @internal\n */\nfunction schemaWire(\n format: IAiStructuredOutputCapability['format'],\n raw: JsonValue\n): IResolvedStructuredOutput {\n switch (format) {\n case 'openai-json-schema':\n return {\n enforcement: 'schema',\n wire: {\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'response', strict: true, schema: raw }\n }\n }\n };\n case 'openai-responses-format':\n // The Responses API nests the same choice under `text.format` and flattens the\n // schema onto the format object rather than a `json_schema` sub-object.\n return {\n enforcement: 'schema',\n wire: { text: { format: { type: 'json_schema', name: 'response', strict: true, schema: raw } } }\n };\n case 'gemini-response-schema':\n // Merged into `generationConfig`, not the body. Gemini's schema is an\n // OpenAPI-3.0 subset that REJECTS draft-07 keywords rather than ignoring them,\n // and `JsonSchema` is strict-by-default so `.toJson()` emits\n // `additionalProperties: false` on every object node — hence the same sanitizer\n // the tool path uses.\n return {\n enforcement: 'schema',\n wire: { responseMimeType: 'application/json', responseSchema: toGeminiParameterSchema(raw) }\n };\n case 'anthropic-tool-forced':\n // Anthropic has no response-format field. The schema becomes a synthetic\n // tool's `input_schema` and `tool_choice` forces it, which is why this is a\n // distinct enforcement value rather than a spelling of `'schema'`: the reply\n // arrives in a `tool_use` block, not as text.\n return {\n enforcement: 'tool-forced',\n wire: {\n tools: [\n {\n name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,\n description: 'Return the response as structured data matching the supplied schema.',\n input_schema: raw\n }\n ],\n tool_choice: { type: 'tool', name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME }\n }\n };\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Wire fields for a bare JSON-object request, or `undefined` when the format\n * cannot express one.\n *\n * @remarks\n * The `undefined` return **is** the capability table — there is deliberately no\n * separate `supportsJsonObject` flag anywhere, because a second declaration of\n * what a format can do could only ever disagree with this function.\n * @internal\n */\nfunction jsonObjectWire(\n format: IAiStructuredOutputCapability['format']\n): IResolvedStructuredOutput | undefined {\n switch (format) {\n case 'openai-json-schema':\n return { enforcement: 'json-mode', wire: { response_format: { type: 'json_object' } } };\n case 'openai-responses-format':\n return { enforcement: 'json-mode', wire: { text: { format: { type: 'json_object' } } } };\n case 'gemini-response-schema':\n return { enforcement: 'json-mode', wire: { responseMimeType: 'application/json' } };\n case 'anthropic-tool-forced':\n // A forced tool needs an input schema to be forced *to*, so there is no\n // schema-less form of this mechanism.\n return undefined;\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Whether `raw` declares any object property that is absent from that object's\n * `required` list — at any depth.\n *\n * @remarks\n * **This is a hard constraint of OpenAI's strict structured output, not a style\n * preference.** `response_format: { type: 'json_schema', json_schema: { strict: true } }`\n * requires *every* key in `properties` to appear in `required`; a schema that omits\n * one is rejected with a 400 before the model ever runs. `JsonSchema.optional(...)`\n * produces exactly that shape, so an authored schema with one optional field is\n * unsendable to the two OpenAI strict formats.\n *\n * The three obvious repairs are all worse than refusing. Rewriting optional to\n * required-and-nullable changes what the model must emit (`null` rather than\n * omission), so the reply would no longer satisfy the caller's own validator —\n * breaking the one-object-cannot-drift property this whole surface exists for.\n * Dropping `strict` silently downgrades the guarantee while still reporting\n * `'schema'`, which is the lie the required report exists to prevent. And sending\n * it anyway just relocates the failure to an opaque provider 400.\n *\n * So this is treated as a **capability mismatch** and routed through the caller's\n * existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly\n * on request. Gemini and Anthropic have no such rule and are unaffected.\n *\n * **One narrow exception, and it does not weaken the above.** The first repair is\n * unsafe *because the rewritten schema admits a reply the original rejects*. When\n * the optional property's node **already admits `null`**, as it does when authored\n * `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so\n * listing the key in `required` only removes the model's option to omit it, and\n * every reply the emitted schema permits still satisfies the supplied one. That\n * case is hoisted by {@link hoistNullableOptionals} when the caller opts in via\n * `adaptOptionalToNullable`, **and this function is then re-run on the result** —\n * so a property that is genuinely not `null`-able still lands here and still\n * refuses. The condition is read off the schema, never asserted by the caller.\n * @internal\n */\nexport function hasOptionalProperties(raw: JsonValue): boolean {\n if (Array.isArray(raw)) {\n return raw.some(hasOptionalProperties);\n }\n if (raw === null || typeof raw !== 'object') {\n return false;\n }\n const properties = raw.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: ReadonlyArray<JsonValue> = Array.isArray(raw.required) ? raw.required : [];\n for (const name of Object.keys(properties)) {\n if (!required.includes(name)) {\n return true;\n }\n }\n }\n return Object.values(raw).some(hasOptionalProperties);\n}\n\n/** Whether a wire node's `type` admits `null` — either spelling. @internal */\nfunction admitsNull(node: JsonValue): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n return false;\n }\n return Array.isArray(node.type) && node.type.includes('null');\n}\n\n/**\n * Rewrites `raw` so that every optional property whose node already admits `null`\n * is listed in its parent's `required` array, at any depth.\n *\n * @remarks\n * The rewrite is deliberately **narrow, and its narrowness is the safety argument.**\n * `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored\n * as `optional(string({ nullable: true }))` is already `['string', 'null']` on the\n * wire and differs from its required sibling only by absence from `required`.\n * Adding it there narrows the permitted replies from *absent-or-null-or-value* to\n * *null-or-value* — a strict subset of what the caller's own schema accepts. No\n * reply that satisfies the emitted schema can fail the supplied one.\n *\n * A property whose node does not admit `null` is left exactly as it was, which is\n * what makes this composable with the existing guard rather than a replacement for\n * it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable\n * optional still routes through `onUnsupported`. **The verification is the original\n * check, applied again** — there is no second notion of correctness to keep in sync.\n * @internal\n */\nexport function hoistNullableOptionals(raw: JsonValue): JsonValue {\n if (Array.isArray(raw)) {\n return raw.map(hoistNullableOptionals);\n }\n if (raw === null || typeof raw !== 'object') {\n return raw;\n }\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(raw)) {\n out[key] = hoistNullableOptionals(value);\n }\n\n const properties = out.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: JsonValue[] = Array.isArray(out.required) ? [...out.required] : [];\n for (const [name, propSchema] of Object.entries(properties)) {\n if (!required.includes(name) && admitsNull(propSchema)) {\n required.push(name);\n }\n }\n if (required.length > 0) {\n out.required = required;\n }\n }\n return out;\n}\n\n/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */\nfunction isOpenAiStrictFormat(format: IAiStructuredOutputCapability['format']): boolean {\n return format === 'openai-json-schema' || format === 'openai-responses-format';\n}\n\n/**\n * Whether a resolved wire claims the provider's tools channel, and therefore\n * genuinely conflicts with server-side tools.\n *\n * @remarks\n * Asked of the **resolved wire** rather than the declared format, because a format\n * that *would* claim the channel does not claim it when the request degraded to\n * sending nothing. Anthropic + `json-object` is exactly that case: the mode has no\n * expression there, so the wire is empty and there is nothing to conflict with —\n * rejecting it would refuse a request that was about to become harmless.\n * @internal\n */\nfunction conflictsWithServerTools(\n format: IAiStructuredOutputCapability['format'],\n resolved: IResolvedStructuredOutput\n): boolean {\n return (\n resolved.enforcement !== 'none' &&\n (format === 'anthropic-tool-forced' || format === 'gemini-response-schema')\n );\n}\n\n/**\n * The wire format actually in force, given which OpenAI endpoint the dispatcher\n * will use.\n *\n * @remarks\n * **The OpenAI route is not a function of the model alone.** `callProviderCompletion`\n * sends a request to `/responses` when it carries server tools **or** when the model\n * is Responses-only, and to `/chat/completions` otherwise — so the same model takes\n * different endpoints on different calls, and those endpoints spell structured output\n * differently (`response_format` vs `text.format`). A capability declaration keyed on\n * the model therefore cannot name the right one by itself, and emitting\n * `response_format` into a `/responses` body would be silently ignored by the\n * provider: the request would look constrained and the reply would not be, with the\n * report confidently saying `'schema'`.\n *\n * The declaration still names each family's *support*; this is the one axis it cannot\n * carry, so it is supplied by the dispatcher that makes the routing decision.\n * @internal\n */\nfunction effectiveFormat(\n declared: IAiStructuredOutputCapability['format'],\n usesResponsesApi: boolean\n): IAiStructuredOutputCapability['format'] {\n if (usesResponsesApi && declared === 'openai-json-schema') {\n return 'openai-responses-format';\n }\n return declared;\n}\n\n/**\n * Resolve a caller's structured-output request against the concrete model that\n * will serve it.\n *\n * @param descriptor - The provider descriptor.\n * @param model - The **concrete** model id, already through `resolveProviderModel`.\n * Passing an alias here would be a bug of the class `resolveImageCapability` once\n * had, where an unresolved alias fell through to a catch-all `modelPrefix: ''` and\n * returned a confidently wrong capability.\n * @param request - The caller's intent, or `undefined` for no request at all.\n * @param serverTools - Server-side tools on the same request, which conflict with\n * structured output on two of the four formats.\n * @param usesResponsesApi - Whether the dispatcher will send this request to the\n * OpenAI Responses API rather than Chat Completions. See {@link effectiveFormat} —\n * the route is not a function of the model alone, so the capability declaration\n * cannot carry it.\n * @returns The decision, or `Failure` when the caller asked to fail rather than\n * degrade — or when the request conflicts with server tools, which is never\n * degradable because the caller asked for two things the provider cannot both do.\n * @internal\n */\nexport function resolveStructuredOutput(\n descriptor: IAiProviderDescriptor,\n model: string,\n request: StructuredOutputRequest | undefined,\n serverTools: ReadonlyArray<AiServerToolConfig> | undefined,\n usesResponsesApi: boolean,\n resolveCapability: (\n descriptor: IAiProviderDescriptor,\n model: string\n ) => IAiStructuredOutputCapability | undefined\n): Result<IResolvedStructuredOutput> {\n if (request === undefined) {\n return succeed(NO_STRUCTURED_OUTPUT);\n }\n const fallback: StructuredOutputFallback = request.onUnsupported ?? 'degrade';\n const capability = resolveCapability(descriptor, model);\n if (capability === undefined) {\n return fallback === 'fail'\n ? fail(\n `provider '${descriptor.id}' model '${model}' declares no structured-output capability; ` +\n `pass onUnsupported: 'degrade' to send the request unconstrained`\n )\n : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n const format = effectiveFormat(capability.format, usesResponsesApi);\n\n // Resolve the wire FIRST, then judge conflicts against what it actually is.\n // Ordering matters: a format that would claim the tools channel does not claim\n // it when the request degraded to sending nothing.\n let resolved: IResolvedStructuredOutput | undefined;\n let unsupported: string | undefined;\n if (request.mode === 'schema') {\n // Hoist BEFORE the guard, then let the guard judge the result. The rewrite only\n // ever removes optionality that was safe to remove, so re-running the original\n // check is the whole verification — a schema that still trips it was not\n // adaptable, and refuses exactly as it did before the flag existed.\n // Gated on the format, not just the flag: hoisting narrows what the model may\n // send, so applying it where the all-required rule does not exist would change\n // a reply on a provider that never needed it changed.\n const strict: boolean = isOpenAiStrictFormat(format);\n const adapt: boolean = strict && request.adaptOptionalToNullable === true;\n const raw: JsonValue = adapt ? hoistNullableOptionals(request.schema.toJson()) : request.schema.toJson();\n if (strict && hasOptionalProperties(raw)) {\n // See `hasOptionalProperties` — a hard provider constraint, treated as a\n // capability mismatch rather than relocated into an opaque 400.\n unsupported =\n `the supplied schema declares optional properties, and OpenAI strict structured output ` +\n `requires every property to be required; ` +\n (adapt\n ? `adaptOptionalToNullable hoisted the ones that admit null, but at least one does not — ` +\n `author it as nullable (e.g. optional(string({ nullable: true }))) so null is an ` +\n `accepted reply, make it required, or pass `\n : `author them as required, adopt adaptOptionalToNullable if null is an accepted reply ` +\n `for each of them, or pass `) +\n `onUnsupported: 'degrade' to send the request unconstrained`;\n } else {\n resolved = schemaWire(format, raw);\n }\n } else {\n resolved = jsonObjectWire(format);\n if (resolved === undefined) {\n // Today this is only `'json-object'` on Anthropic, whose mechanism needs a\n // schema to force a tool to.\n unsupported = `provider '${descriptor.id}' model '${model}' cannot enforce '${request.mode}' structured output`;\n }\n }\n\n if (resolved === undefined) {\n return fallback === 'fail' ? fail(`${unsupported}`) : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n // Two formats cannot carry structured output and server-side tools at once, for\n // DIFFERENT reasons — worth separating, because a reader who assumes one\n // mechanism will reason wrongly about the other.\n //\n // anthropic-tool-forced: a wire-level clash. The constraint IS `tools` +\n // `tool_choice`, so server tools would be overwritten (and `tool_choice`\n // forces ours, which disables theirs anyway).\n // gemini-response-schema: NOT a wire clash — `responseMimeType` /\n // `responseSchema` live in `generationConfig`, nowhere near `tools`. It is\n // an API-level mutual exclusivity Gemini enforces, the same restriction the\n // client-tool path already pre-empts.\n //\n // Neither is degradable: silently dropping either half would give the caller\n // something they did not ask for, and `onUnsupported` speaks to what a model can\n // enforce, not to a caller asking for two incompatible things.\n if (serverTools !== undefined && serverTools.length > 0 && conflictsWithServerTools(format, resolved)) {\n const why =\n format === 'anthropic-tool-forced'\n ? 'Anthropic enforces structured output by forcing a tool, so it cannot be combined with'\n : 'Gemini cannot combine a response schema with';\n return fail(\n `${why} server-side tools (${serverTools.map((t) => t.type).join(', ')}) in the same request; ` +\n `send one or the other`\n );\n }\n\n return succeed(resolved);\n}\n\n/**\n * Every valid `StructuredOutputEnforcement`, for the wire-shape guard below.\n *\n * @remarks\n * A **total** `Record`, not a `Set` built from an array literal — the same reasoning\n * as `SCHEMA_NODE_TYPES` in `@fgv/ts-json-base`. A `Set` catches a removed or\n * misspelled member but not an *added* one, so a new enforcement value would compile\n * fine here while this guard silently began rejecting it off a proxy response. The\n * `Record` makes that addition a compile error at this line.\n * @internal\n */\nconst ENFORCEMENTS: Readonly<Record<StructuredOutputEnforcement, true>> = {\n none: true,\n 'json-mode': true,\n schema: true,\n 'tool-forced': true\n};\n\n/**\n * Whether an untyped value off a proxy response is a valid\n * `StructuredOutputEnforcement`.\n * @internal\n */\nexport function isStructuredOutputEnforcement(value: unknown): value is StructuredOutputEnforcement {\n // Indexed read compared to `true`, NOT `in` — `in` walks the prototype chain, so a\n // proxy answering `structuredOutput: 'constructor'` would pass it.\n return typeof value === 'string' && ENFORCEMENTS[value as StructuredOutputEnforcement] === true;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"structuredOutputTypes.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Structured-output types: the capability a provider declares, the request a\n * caller makes, and the enforcement the response reports.\n *\n * @remarks\n * Their own module rather than part of `model.ts` because they depend on nothing\n * there — `model.ts` imports them, not the reverse — and because `model.ts` was\n * at the `max-lines` cap. A dependency-free cut is one of the few available at a\n * moment like that which is not chosen under pressure.\n * @packageDocumentation\n */\n\nimport { type JsonSchema } from '@fgv/ts-json-base';\n\n// ============================================================================\n// Structured output — capability\n// ============================================================================\n\n/**\n * Wire format a provider uses to express a structured-output constraint.\n *\n * @remarks\n * Four shapes, not one, and they differ in more than field names: the OpenAI\n * pair carry the schema in the request body, Gemini carries it inside\n * `generationConfig`, and Anthropic has no response-format field at all —\n * its mechanism is forced tool use, which is why `'tool-forced'` is a distinct\n * {@link AiAssist.StructuredOutputEnforcement} value rather than a spelling of\n * `'schema'`.\n * @public\n */\nexport type AiStructuredOutputFormat =\n | 'openai-json-schema'\n | 'openai-responses-format'\n | 'gemini-response-schema'\n | 'anthropic-tool-forced';\n\n/**\n * Structured-output capability for a model family within a provider. Used as an\n * entry in `IAiProviderDescriptor.structuredOutput`.\n *\n * @remarks\n * Deliberately thinner than its `imageGeneration` / `embedding` siblings: it\n * carries no `supportsX` flags, because what each format can enforce is a\n * property of the provider's **API surface** rather than of any one model, and a\n * per-entry declaration of it could only ever disagree with the one in code.\n * @public\n */\nexport interface IAiStructuredOutputCapability {\n /**\n * Prefix matched against the resolved completion model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes match a\n * model id, the longest prefix wins; ties are broken by first-encountered.\n */\n readonly modelPrefix: string;\n /** Wire format used to express the constraint for matching models. */\n readonly format: AiStructuredOutputFormat;\n}\n\n/**\n * Which constraint the provider was **asked** to apply to this response.\n *\n * @remarks\n * Three questions hide inside *\"did it honour my schema\"*, and they have different\n * owners:\n *\n * | question | answerable by |\n * |---|---|\n * | did we send a constraint? | this client, at request-build time |\n * | which constraint did the provider apply? | this client, from the resolved model's capability |\n * | does *this response* conform to my shape? | the caller's converter, and nothing else |\n *\n * This type answers the first two and deliberately not the third. Reporting\n * conformance would mean re-validating against the caller's own schema to\n * re-derive an answer the caller already holds.\n *\n * - `'none'` — nothing was sent; the resolved model declares no capability.\n * - `'json-mode'` — syntactically valid JSON is guaranteed; the shape is not.\n * - `'schema'` — generation was constrained to the supplied schema.\n * - `'tool-forced'` — Anthropic-style forced tool use; the shape comes from the\n * forced tool's input schema, and `content` is the re-serialized tool input.\n * @public\n */\nexport type StructuredOutputEnforcement = 'none' | 'json-mode' | 'schema' | 'tool-forced';\n\n/**\n * What to do when the resolved model cannot apply the requested constraint.\n *\n * @remarks\n * `'degrade'` is the default, and it is only safe **because\n * `IAiCompletionResponse.structuredOutput` is required** rather than\n * optional. Degrade-and-tell-me is safe; degrade-silently is the failure this\n * whole surface exists to remove — so the two decisions are one decision, not\n * two independent ones.\n *\n * Reach for `'fail'` when the output is persisted or put on a wire, where an\n * unconstrained generation that happens to parse is worse than an error because\n * it is wrong quietly. Leave it at `'degrade'` on paths that are *designed* to\n * degrade — an extractor that may return nothing, a segmenter that floors to a\n * mechanical chunker — where a hard failure would make this library less safe\n * than the code it replaces.\n * @public\n */\nexport type StructuredOutputFallback = 'degrade' | 'fail';\n\n/**\n * Ask the provider for JSON constrained to a schema.\n * @public\n */\nexport interface ISchemaStructuredOutputRequest {\n readonly mode: 'schema';\n /**\n * The schema to constrain generation to — **the same object you validate the\n * reply with**, so the wire schema and the check cannot drift.\n *\n * @remarks\n * Author it with `JsonSchema.object({...})` from `@fgv/ts-json-base`. This is\n * the property `@fgv/ts-extras-ollama`'s `chatStructured` already has; this\n * surface is its cloud sibling.\n */\n readonly schema: JsonSchema.ISchemaValidator<unknown>;\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * Ask the provider for syntactically valid JSON of arbitrary shape.\n *\n * @remarks\n * The weaker floor, and worth having on its own: the failure that motivated this\n * surface (`Expected ',' or '}' after property value` — an unescaped quote closing\n * a string early) is **syntactic**, so a JSON-mode guarantee removes it. Schema\n * constraint is what additionally buys shape. It is also the only mode some\n * model/provider pairs support.\n * @public\n */\nexport interface IJsonObjectStructuredOutputRequest {\n readonly mode: 'json-object';\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * A caller's structured-output intent.\n *\n * @remarks\n * A discriminated union rather than an optional `schema` whose absence means\n * *\"json-object please\"* — an absence that means something is the shape this repo\n * has been burned by (see `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, which\n * exists because a three-ways-ambiguous absence could not be read).\n *\n * **The caller supplies intent; the response reports outcome.** A request never\n * needs to know whether the constraint will be honoured, because\n * `resolveProviderModel` resolves aliases and tiers at *call* time — a `tier`\n * request can cascade — so the concrete model that will serve a request is not\n * knowable to the caller up front. Requiring it to know would be unsound, which\n * is why the report rides on the response rather than being a lookup.\n * @public\n */\nexport type StructuredOutputRequest = ISchemaStructuredOutputRequest | IJsonObjectStructuredOutputRequest;\n"]}
1
+ {"version":3,"file":"structuredOutputTypes.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Structured-output types: the capability a provider declares, the request a\n * caller makes, and the enforcement the response reports.\n *\n * @remarks\n * Their own module rather than part of `model.ts` because they depend on nothing\n * there — `model.ts` imports them, not the reverse — and because `model.ts` was\n * at the `max-lines` cap. A dependency-free cut is one of the few available at a\n * moment like that which is not chosen under pressure.\n * @packageDocumentation\n */\n\nimport { type JsonSchema } from '@fgv/ts-json-base';\n\n// ============================================================================\n// Structured output — capability\n// ============================================================================\n\n/**\n * Wire format a provider uses to express a structured-output constraint.\n *\n * @remarks\n * Four shapes, not one, and they differ in more than field names: the OpenAI\n * pair carry the schema in the request body, Gemini carries it inside\n * `generationConfig`, and Anthropic has no response-format field at all —\n * its mechanism is forced tool use, which is why `'tool-forced'` is a distinct\n * {@link AiAssist.StructuredOutputEnforcement} value rather than a spelling of\n * `'schema'`.\n * @public\n */\nexport type AiStructuredOutputFormat =\n | 'openai-json-schema'\n | 'openai-responses-format'\n | 'gemini-response-schema'\n | 'anthropic-tool-forced';\n\n/**\n * Structured-output capability for a model family within a provider. Used as an\n * entry in `IAiProviderDescriptor.structuredOutput`.\n *\n * @remarks\n * Deliberately thinner than its `imageGeneration` / `embedding` siblings: it\n * carries no `supportsX` flags, because what each format can enforce is a\n * property of the provider's **API surface** rather than of any one model, and a\n * per-entry declaration of it could only ever disagree with the one in code.\n * @public\n */\nexport interface IAiStructuredOutputCapability {\n /**\n * Prefix matched against the resolved completion model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes match a\n * model id, the longest prefix wins; ties are broken by first-encountered.\n */\n readonly modelPrefix: string;\n /** Wire format used to express the constraint for matching models. */\n readonly format: AiStructuredOutputFormat;\n}\n\n/**\n * Which constraint the provider was **asked** to apply to this response.\n *\n * @remarks\n * Three questions hide inside *\"did it honour my schema\"*, and they have different\n * owners:\n *\n * | question | answerable by |\n * |---|---|\n * | did we send a constraint? | this client, at request-build time |\n * | which constraint did the provider apply? | this client, from the resolved model's capability |\n * | does *this response* conform to my shape? | the caller's converter, and nothing else |\n *\n * This type answers the first two and deliberately not the third. Reporting\n * conformance would mean re-validating against the caller's own schema to\n * re-derive an answer the caller already holds.\n *\n * - `'none'` — nothing was sent; the resolved model declares no capability.\n * - `'json-mode'` — syntactically valid JSON is guaranteed; the shape is not.\n * - `'schema'` — generation was constrained to the supplied schema.\n * - `'tool-forced'` — Anthropic-style forced tool use; the shape comes from the\n * forced tool's input schema, and `content` is the re-serialized tool input.\n * @public\n */\nexport type StructuredOutputEnforcement = 'none' | 'json-mode' | 'schema' | 'tool-forced';\n\n/**\n * What to do when the resolved model cannot apply the requested constraint.\n *\n * @remarks\n * `'degrade'` is the default, and it is only safe **because\n * `IAiCompletionResponse.structuredOutput` is required** rather than\n * optional. Degrade-and-tell-me is safe; degrade-silently is the failure this\n * whole surface exists to remove — so the two decisions are one decision, not\n * two independent ones.\n *\n * Reach for `'fail'` when the output is persisted or put on a wire, where an\n * unconstrained generation that happens to parse is worse than an error because\n * it is wrong quietly. Leave it at `'degrade'` on paths that are *designed* to\n * degrade — an extractor that may return nothing, a segmenter that floors to a\n * mechanical chunker — where a hard failure would make this library less safe\n * than the code it replaces.\n * @public\n */\nexport type StructuredOutputFallback = 'degrade' | 'fail';\n\n/**\n * Ask the provider for JSON constrained to a schema.\n * @public\n */\nexport interface ISchemaStructuredOutputRequest {\n readonly mode: 'schema';\n /**\n * The schema to constrain generation to — **the same object you validate the\n * reply with**, so the wire schema and the check cannot drift.\n *\n * @remarks\n * Author it with `JsonSchema.object({...})` from `@fgv/ts-json-base`. This is\n * the property `@fgv/ts-extras-ollama`'s `chatStructured` already has; this\n * surface is its cloud sibling.\n */\n readonly schema: JsonSchema.ISchemaValidator<unknown>;\n readonly onUnsupported?: StructuredOutputFallback;\n /**\n * On a format that requires every property to be `required`, send an optional\n * property as required when its node **already admits `null`**, instead of\n * refusing the whole schema.\n *\n * @remarks\n * `JsonSchema.optional(...)` emits its inner node verbatim — optionality lives\n * only in the parent's `required` array — so for a property authored as\n * `optional(string({ nullable: true }))` the wire node is already\n * `['string', 'null']`, and adding the key to `required` narrows what the model\n * may send from *absent-or-null-or-value* to *null-or-value*. **Every reply the\n * narrowed schema permits, the original schema already accepted**, so the\n * one-object-cannot-drift property is preserved rather than traded away.\n *\n * That is why this is not the caller asserting its validator tolerates `null`.\n * An assertion could be false; this is read off the schema. A property authored\n * as plain `optional(string())` rejects `null`, is therefore **not** hoistable,\n * and the schema still refuses through `onUnsupported` exactly as before — with\n * an error naming the properties that blocked it. Setting this flag can never\n * produce a wire schema the supplied schema would reject.\n *\n * Opt-in because it is still a **semantic** change to the reply: the model must\n * now emit `null` where it could previously omit the key. A caller that\n * distinguishes those two — rather than treating them alike, as\n * `optional(nullable)` says it does — should leave this off.\n *\n * Defaults to `false`. The enforcement report is unaffected: a schema that goes\n * out reports `'schema'` whether or not any property was hoisted.\n */\n readonly adaptOptionalToNullable?: boolean;\n}\n\n/**\n * Ask the provider for syntactically valid JSON of arbitrary shape.\n *\n * @remarks\n * The weaker floor, and worth having on its own: the failure that motivated this\n * surface (`Expected ',' or '}' after property value` — an unescaped quote closing\n * a string early) is **syntactic**, so a JSON-mode guarantee removes it. Schema\n * constraint is what additionally buys shape. It is also the only mode some\n * model/provider pairs support.\n * @public\n */\nexport interface IJsonObjectStructuredOutputRequest {\n readonly mode: 'json-object';\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * A caller's structured-output intent.\n *\n * @remarks\n * A discriminated union rather than an optional `schema` whose absence means\n * *\"json-object please\"* — an absence that means something is the shape this repo\n * has been burned by (see `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, which\n * exists because a three-ways-ambiguous absence could not be read).\n *\n * **The caller supplies intent; the response reports outcome.** A request never\n * needs to know whether the constraint will be honoured, because\n * `resolveProviderModel` resolves aliases and tiers at *call* time — a `tier`\n * request can cascade — so the concrete model that will serve a request is not\n * knowable to the caller up front. Requiring it to know would be unsound, which\n * is why the report rides on the response rather than being a lookup.\n * @public\n */\nexport type StructuredOutputRequest = ISchemaStructuredOutputRequest | IJsonObjectStructuredOutputRequest;\n"]}
@@ -41,7 +41,10 @@ export function providerDiscriminatorForId(providerId) {
41
41
  // Common-subset mapping
42
42
  // ============================================================================
43
43
  /**
44
- * Maps generic effort to Anthropic wire effort. @internal
44
+ * Maps generic effort to Anthropic wire effort. Anthropic has no 'none' value in its
45
+ * own vocabulary — the caller (tier 1 of `mergeThinkingConfig`) handles `'none'` by
46
+ * skipping this mapping entirely, so `anthropicEffort` stays unset and the emit site
47
+ * omits the `thinking` wire param. @internal
45
48
  */
46
49
  function genericEffortToAnthropic(effort) {
47
50
  return effort; // 1:1 mapping for the common subset
@@ -76,10 +79,16 @@ function genericEffortToOpenAi(effort) {
76
79
  return effort; // 1:1 mapping for the common subset
77
80
  }
78
81
  /**
79
- * Maps generic effort to Gemini thinkingBudget. @internal
82
+ * Maps generic effort to Gemini thinkingBudget. `'none'` maps to `0` — Gemini's own
83
+ * off value on the wire, though `IGeminiThinkingConfig.thinkingBudget` documents that `0`
84
+ * is valid only on Flash/Flash-Lite and errors on Pro; this mapping is not model-aware and
85
+ * inherits that same caveat (not a new gap — an explicit `providers` block could already
86
+ * request `thinkingBudget: 0` on any model). @internal
80
87
  */
81
88
  function genericEffortToGemini(effort) {
82
89
  switch (effort) {
90
+ case 'none':
91
+ return 0;
83
92
  case 'low':
84
93
  return 1024;
85
94
  case 'medium':
@@ -170,7 +179,13 @@ export function mergeThinkingConfig(config, resolvedModel, discriminator) {
170
179
  if (config.effort !== undefined) {
171
180
  switch (discriminator) {
172
181
  case 'anthropic':
173
- resolved = Object.assign(Object.assign({}, resolved), { anthropicEffort: genericEffortToAnthropic(config.effort) });
182
+ // Anthropic has no 'none' value: "off" means no `thinking` wire param at all,
183
+ // so 'none' leaves `anthropicEffort` unset rather than mapping to a value. That
184
+ // also satisfies checkTemperatureConflict's anthropic gate (which fails only when
185
+ // anthropicEffort is set), so temperature survives without any special-casing there.
186
+ if (config.effort !== 'none') {
187
+ resolved = Object.assign(Object.assign({}, resolved), { anthropicEffort: genericEffortToAnthropic(config.effort) });
188
+ }
174
189
  break;
175
190
  case 'openai':
176
191
  resolved = Object.assign(Object.assign({}, resolved), { openAiEffort: genericEffortToOpenAi(config.effort) });