@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.
- package/CAPABILITIES.md +95 -0
- package/dist/packlets/ai-assist/cacheRequest.js +118 -0
- package/dist/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/dist/packlets/ai-assist/chatRequestBuilders.js +70 -5
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/completionClient.js +122 -45
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/dist/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/dist/packlets/ai-assist/index.js +3 -1
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +200 -0
- package/dist/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js +137 -0
- package/dist/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js +156 -0
- package/dist/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js +124 -0
- package/dist/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/dist/packlets/ai-assist/registry.js +9 -11
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamUsageCapability.js +124 -0
- package/dist/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +24 -5
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -33
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +34 -8
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +22 -14
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +3 -2
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/structuredOutput.js +84 -7
- package/dist/packlets/ai-assist/structuredOutput.js.map +1 -1
- package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolFormats.js +80 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/packlets/ai-assist/toolTypes.js +21 -0
- package/dist/packlets/ai-assist/toolTypes.js.map +1 -0
- package/dist/packlets/ai-assist/usageNormalization.js +178 -0
- package/dist/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/dist/packlets/ai-assist/usageTypes.js +21 -0
- package/dist/packlets/ai-assist/usageTypes.js.map +1 -0
- package/dist/packlets/mustache/index.js.map +1 -1
- package/dist/packlets/mustache/interfaces.js.map +1 -1
- package/dist/packlets/mustache/mustacheTemplate.js +91 -0
- package/dist/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/dist/ts-extras.d.ts +577 -30
- package/lib/packlets/ai-assist/cacheRequest.d.ts +100 -0
- package/lib/packlets/ai-assist/cacheRequest.d.ts.map +1 -0
- package/lib/packlets/ai-assist/cacheRequest.js +123 -0
- package/lib/packlets/ai-assist/cacheRequest.js.map +1 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +34 -4
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +73 -5
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/completionClient.d.ts +6 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +121 -44
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js +1 -1
- package/lib/packlets/ai-assist/imageGenerationClient.js.map +1 -1
- package/lib/packlets/ai-assist/index.d.ts +7 -2
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +11 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +59 -288
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts +124 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js +208 -0
- package/lib/packlets/ai-assist/promptCacheAnthropicBreakpointHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts +77 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js +143 -0
- package/lib/packlets/ai-assist/promptCacheObservabilityHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts +84 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js +163 -0
- package/lib/packlets/ai-assist/promptCacheRoutingAbHarness.js.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts +105 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.d.ts.map +1 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js +130 -0
- package/lib/packlets/ai-assist/promptCacheUsageMath.js.map +1 -0
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +9 -11
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts +119 -0
- package/lib/packlets/ai-assist/streamUsageCapability.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js +130 -0
- package/lib/packlets/ai-assist/streamUsageCapability.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +23 -4
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +21 -12
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +64 -34
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +21 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js +25 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -5
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +33 -7
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +21 -13
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +3 -2
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/structuredOutput.d.ts +32 -0
- package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -1
- package/lib/packlets/ai-assist/structuredOutput.js +85 -7
- package/lib/packlets/ai-assist/structuredOutput.js.map +1 -1
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +30 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -1
- package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +18 -3
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts +55 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +82 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/lib/packlets/ai-assist/toolTypes.d.ts +328 -0
- package/lib/packlets/ai-assist/toolTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/toolTypes.js +22 -0
- package/lib/packlets/ai-assist/toolTypes.js.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts +58 -0
- package/lib/packlets/ai-assist/usageNormalization.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageNormalization.js +184 -0
- package/lib/packlets/ai-assist/usageNormalization.js.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts +84 -0
- package/lib/packlets/ai-assist/usageTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/usageTypes.js +22 -0
- package/lib/packlets/ai-assist/usageTypes.js.map +1 -0
- package/lib/packlets/mustache/index.d.ts +1 -1
- package/lib/packlets/mustache/index.d.ts.map +1 -1
- package/lib/packlets/mustache/index.js.map +1 -1
- package/lib/packlets/mustache/interfaces.d.ts +30 -0
- package/lib/packlets/mustache/interfaces.d.ts.map +1 -1
- package/lib/packlets/mustache/interfaces.js.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts +56 -1
- package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -1
- package/lib/packlets/mustache/mustacheTemplate.js +91 -0
- package/lib/packlets/mustache/mustacheTemplate.js.map +1 -1
- package/package.json +16 -16
- package/perf/promptCacheAnthropicBreakpoint.js +135 -0
- package/perf/promptCacheObservability.js +107 -0
- package/perf/promptCacheRoutingAb.js +105 -0
|
@@ -33,6 +33,7 @@ const ts_utils_1 = require("@fgv/ts-utils");
|
|
|
33
33
|
const chatRequestBuilders_1 = require("./chatRequestBuilders");
|
|
34
34
|
const endpoint_1 = require("./endpoint");
|
|
35
35
|
const model_1 = require("./model");
|
|
36
|
+
const streamUsageCapability_1 = require("./streamUsageCapability");
|
|
36
37
|
const anthropic_1 = require("./streamingAdapters/anthropic");
|
|
37
38
|
const gemini_1 = require("./streamingAdapters/gemini");
|
|
38
39
|
const openaiChat_1 = require("./streamingAdapters/openaiChat");
|
|
@@ -129,9 +130,9 @@ async function callProviderCompletionStream(params) {
|
|
|
129
130
|
// Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
|
|
130
131
|
// to the Responses stream even with no tools requested — same path the tools case uses.
|
|
131
132
|
if (hasTools || (0, model_1.isResponsesOnlyModel)(descriptor, config.model)) {
|
|
132
|
-
return (0, openaiResponses_1.callOpenAiResponsesStream)(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens);
|
|
133
|
+
return (0, openaiResponses_1.callOpenAiResponsesStream)(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, undefined, undefined, maxTokens, (0, streamUsageCapability_1.supportsCacheUsageReporting)(descriptor));
|
|
133
134
|
}
|
|
134
|
-
return (0, openaiChat_1.callOpenAiChatStream)(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, (0, model_1.usesMaxCompletionTokensField)(descriptor));
|
|
135
|
+
return (0, openaiChat_1.callOpenAiChatStream)(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, (0, model_1.usesMaxCompletionTokensField)(descriptor), (0, streamUsageCapability_1.supportsStreamUsageOption)(descriptor));
|
|
135
136
|
case 'anthropic':
|
|
136
137
|
return (0, anthropic_1.callAnthropicStream)(config, prompt, head, temperature, tools, logger, signal, resolvedThinking, undefined, undefined, (0, model_1.isAdaptiveThinkingModel)(descriptor, config.model), maxTokens);
|
|
137
138
|
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"]}
|
|
@@ -54,9 +54,41 @@ export declare const NO_STRUCTURED_OUTPUT: IResolvedStructuredOutput;
|
|
|
54
54
|
* So this is treated as a **capability mismatch** and routed through the caller's
|
|
55
55
|
* existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly
|
|
56
56
|
* on request. Gemini and Anthropic have no such rule and are unaffected.
|
|
57
|
+
*
|
|
58
|
+
* **One narrow exception, and it does not weaken the above.** The first repair is
|
|
59
|
+
* unsafe *because the rewritten schema admits a reply the original rejects*. When
|
|
60
|
+
* the optional property's node **already admits `null`**, as it does when authored
|
|
61
|
+
* `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so
|
|
62
|
+
* listing the key in `required` only removes the model's option to omit it, and
|
|
63
|
+
* every reply the emitted schema permits still satisfies the supplied one. That
|
|
64
|
+
* case is hoisted by {@link hoistNullableOptionals} when the caller opts in via
|
|
65
|
+
* `adaptOptionalToNullable`, **and this function is then re-run on the result** —
|
|
66
|
+
* so a property that is genuinely not `null`-able still lands here and still
|
|
67
|
+
* refuses. The condition is read off the schema, never asserted by the caller.
|
|
57
68
|
* @internal
|
|
58
69
|
*/
|
|
59
70
|
export declare function hasOptionalProperties(raw: JsonValue): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Rewrites `raw` so that every optional property whose node already admits `null`
|
|
73
|
+
* is listed in its parent's `required` array, at any depth.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* The rewrite is deliberately **narrow, and its narrowness is the safety argument.**
|
|
77
|
+
* `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored
|
|
78
|
+
* as `optional(string({ nullable: true }))` is already `['string', 'null']` on the
|
|
79
|
+
* wire and differs from its required sibling only by absence from `required`.
|
|
80
|
+
* Adding it there narrows the permitted replies from *absent-or-null-or-value* to
|
|
81
|
+
* *null-or-value* — a strict subset of what the caller's own schema accepts. No
|
|
82
|
+
* reply that satisfies the emitted schema can fail the supplied one.
|
|
83
|
+
*
|
|
84
|
+
* A property whose node does not admit `null` is left exactly as it was, which is
|
|
85
|
+
* what makes this composable with the existing guard rather than a replacement for
|
|
86
|
+
* it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable
|
|
87
|
+
* optional still routes through `onUnsupported`. **The verification is the original
|
|
88
|
+
* check, applied again** — there is no second notion of correctness to keep in sync.
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
export declare function hoistNullableOptionals(raw: JsonValue): JsonValue;
|
|
60
92
|
/**
|
|
61
93
|
* Resolve a caller's structured-output request against the concrete model that
|
|
62
94
|
* will serve it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structuredOutput.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,KAAK,EACV,6BAA6B,EAC7B,2BAA2B,EAE3B,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qCAAqC,EAAE,MAAgC,CAAC;AAErF;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,sCAAsC;IACtC,QAAQ,CAAC,WAAW,EAAE,2BAA2B,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,yBAA6D,CAAC;AAkGjG
|
|
1
|
+
{"version":3,"file":"structuredOutput.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,KAAK,EACV,6BAA6B,EAC7B,2BAA2B,EAE3B,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qCAAqC,EAAE,MAAgC,CAAC;AAErF;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,sCAAsC;IACtC,QAAQ,CAAC,WAAW,EAAE,2BAA2B,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,yBAA6D,CAAC;AAkGjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAiB7D;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,CAyBhE;AA0DD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,GAAG,SAAS,EAC5C,WAAW,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,SAAS,EAC1D,gBAAgB,EAAE,OAAO,EACzB,iBAAiB,EAAE,CACjB,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,6BAA6B,GAAG,SAAS,GAC7C,MAAM,CAAC,yBAAyB,CAAC,CAyFnC;AAoBD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,2BAA2B,CAIlG"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.NO_STRUCTURED_OUTPUT = exports.ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME = void 0;
|
|
8
8
|
exports.hasOptionalProperties = hasOptionalProperties;
|
|
9
|
+
exports.hoistNullableOptionals = hoistNullableOptionals;
|
|
9
10
|
exports.resolveStructuredOutput = resolveStructuredOutput;
|
|
10
11
|
exports.isStructuredOutputEnforcement = isStructuredOutputEnforcement;
|
|
11
12
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
@@ -136,6 +137,17 @@ function jsonObjectWire(format) {
|
|
|
136
137
|
* So this is treated as a **capability mismatch** and routed through the caller's
|
|
137
138
|
* existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly
|
|
138
139
|
* on request. Gemini and Anthropic have no such rule and are unaffected.
|
|
140
|
+
*
|
|
141
|
+
* **One narrow exception, and it does not weaken the above.** The first repair is
|
|
142
|
+
* unsafe *because the rewritten schema admits a reply the original rejects*. When
|
|
143
|
+
* the optional property's node **already admits `null`**, as it does when authored
|
|
144
|
+
* `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so
|
|
145
|
+
* listing the key in `required` only removes the model's option to omit it, and
|
|
146
|
+
* every reply the emitted schema permits still satisfies the supplied one. That
|
|
147
|
+
* case is hoisted by {@link hoistNullableOptionals} when the caller opts in via
|
|
148
|
+
* `adaptOptionalToNullable`, **and this function is then re-run on the result** —
|
|
149
|
+
* so a property that is genuinely not `null`-able still lands here and still
|
|
150
|
+
* refuses. The condition is read off the schema, never asserted by the caller.
|
|
139
151
|
* @internal
|
|
140
152
|
*/
|
|
141
153
|
function hasOptionalProperties(raw) {
|
|
@@ -145,17 +157,68 @@ function hasOptionalProperties(raw) {
|
|
|
145
157
|
if (raw === null || typeof raw !== 'object') {
|
|
146
158
|
return false;
|
|
147
159
|
}
|
|
148
|
-
const
|
|
149
|
-
const properties = node.properties;
|
|
160
|
+
const properties = raw.properties;
|
|
150
161
|
if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
|
|
151
|
-
const required = Array.isArray(
|
|
162
|
+
const required = Array.isArray(raw.required) ? raw.required : [];
|
|
152
163
|
for (const name of Object.keys(properties)) {
|
|
153
164
|
if (!required.includes(name)) {
|
|
154
165
|
return true;
|
|
155
166
|
}
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
return Object.values(
|
|
169
|
+
return Object.values(raw).some(hasOptionalProperties);
|
|
170
|
+
}
|
|
171
|
+
/** Whether a wire node's `type` admits `null` — either spelling. @internal */
|
|
172
|
+
function admitsNull(node) {
|
|
173
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return Array.isArray(node.type) && node.type.includes('null');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Rewrites `raw` so that every optional property whose node already admits `null`
|
|
180
|
+
* is listed in its parent's `required` array, at any depth.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* The rewrite is deliberately **narrow, and its narrowness is the safety argument.**
|
|
184
|
+
* `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored
|
|
185
|
+
* as `optional(string({ nullable: true }))` is already `['string', 'null']` on the
|
|
186
|
+
* wire and differs from its required sibling only by absence from `required`.
|
|
187
|
+
* Adding it there narrows the permitted replies from *absent-or-null-or-value* to
|
|
188
|
+
* *null-or-value* — a strict subset of what the caller's own schema accepts. No
|
|
189
|
+
* reply that satisfies the emitted schema can fail the supplied one.
|
|
190
|
+
*
|
|
191
|
+
* A property whose node does not admit `null` is left exactly as it was, which is
|
|
192
|
+
* what makes this composable with the existing guard rather than a replacement for
|
|
193
|
+
* it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable
|
|
194
|
+
* optional still routes through `onUnsupported`. **The verification is the original
|
|
195
|
+
* check, applied again** — there is no second notion of correctness to keep in sync.
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
function hoistNullableOptionals(raw) {
|
|
199
|
+
if (Array.isArray(raw)) {
|
|
200
|
+
return raw.map(hoistNullableOptionals);
|
|
201
|
+
}
|
|
202
|
+
if (raw === null || typeof raw !== 'object') {
|
|
203
|
+
return raw;
|
|
204
|
+
}
|
|
205
|
+
const out = {};
|
|
206
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
207
|
+
out[key] = hoistNullableOptionals(value);
|
|
208
|
+
}
|
|
209
|
+
const properties = out.properties;
|
|
210
|
+
if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
|
|
211
|
+
const required = Array.isArray(out.required) ? [...out.required] : [];
|
|
212
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
213
|
+
if (!required.includes(name) && admitsNull(propSchema)) {
|
|
214
|
+
required.push(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (required.length > 0) {
|
|
218
|
+
out.required = required;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
159
222
|
}
|
|
160
223
|
/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */
|
|
161
224
|
function isOpenAiStrictFormat(format) {
|
|
@@ -243,13 +306,28 @@ function resolveStructuredOutput(descriptor, model, request, serverTools, usesRe
|
|
|
243
306
|
let resolved;
|
|
244
307
|
let unsupported;
|
|
245
308
|
if (request.mode === 'schema') {
|
|
246
|
-
|
|
247
|
-
|
|
309
|
+
// Hoist BEFORE the guard, then let the guard judge the result. The rewrite only
|
|
310
|
+
// ever removes optionality that was safe to remove, so re-running the original
|
|
311
|
+
// check is the whole verification — a schema that still trips it was not
|
|
312
|
+
// adaptable, and refuses exactly as it did before the flag existed.
|
|
313
|
+
// Gated on the format, not just the flag: hoisting narrows what the model may
|
|
314
|
+
// send, so applying it where the all-required rule does not exist would change
|
|
315
|
+
// a reply on a provider that never needed it changed.
|
|
316
|
+
const strict = isOpenAiStrictFormat(format);
|
|
317
|
+
const adapt = strict && request.adaptOptionalToNullable === true;
|
|
318
|
+
const raw = adapt ? hoistNullableOptionals(request.schema.toJson()) : request.schema.toJson();
|
|
319
|
+
if (strict && hasOptionalProperties(raw)) {
|
|
248
320
|
// See `hasOptionalProperties` — a hard provider constraint, treated as a
|
|
249
321
|
// capability mismatch rather than relocated into an opaque 400.
|
|
250
322
|
unsupported =
|
|
251
323
|
`the supplied schema declares optional properties, and OpenAI strict structured output ` +
|
|
252
|
-
`requires every property to be required;
|
|
324
|
+
`requires every property to be required; ` +
|
|
325
|
+
(adapt
|
|
326
|
+
? `adaptOptionalToNullable hoisted the ones that admit null, but at least one does not — ` +
|
|
327
|
+
`author it as nullable (e.g. optional(string({ nullable: true }))) so null is an ` +
|
|
328
|
+
`accepted reply, make it required, or pass `
|
|
329
|
+
: `author them as required, adopt adaptOptionalToNullable if null is an accepted reply ` +
|
|
330
|
+
`for each of them, or pass `) +
|
|
253
331
|
`onUnsupported: 'degrade' to send the request unconstrained`;
|
|
254
332
|
}
|
|
255
333
|
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"]}
|
|
@@ -105,6 +105,36 @@ export interface ISchemaStructuredOutputRequest {
|
|
|
105
105
|
*/
|
|
106
106
|
readonly schema: JsonSchema.ISchemaValidator<unknown>;
|
|
107
107
|
readonly onUnsupported?: StructuredOutputFallback;
|
|
108
|
+
/**
|
|
109
|
+
* On a format that requires every property to be `required`, send an optional
|
|
110
|
+
* property as required when its node **already admits `null`**, instead of
|
|
111
|
+
* refusing the whole schema.
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* `JsonSchema.optional(...)` emits its inner node verbatim — optionality lives
|
|
115
|
+
* only in the parent's `required` array — so for a property authored as
|
|
116
|
+
* `optional(string({ nullable: true }))` the wire node is already
|
|
117
|
+
* `['string', 'null']`, and adding the key to `required` narrows what the model
|
|
118
|
+
* may send from *absent-or-null-or-value* to *null-or-value*. **Every reply the
|
|
119
|
+
* narrowed schema permits, the original schema already accepted**, so the
|
|
120
|
+
* one-object-cannot-drift property is preserved rather than traded away.
|
|
121
|
+
*
|
|
122
|
+
* That is why this is not the caller asserting its validator tolerates `null`.
|
|
123
|
+
* An assertion could be false; this is read off the schema. A property authored
|
|
124
|
+
* as plain `optional(string())` rejects `null`, is therefore **not** hoistable,
|
|
125
|
+
* and the schema still refuses through `onUnsupported` exactly as before — with
|
|
126
|
+
* an error naming the properties that blocked it. Setting this flag can never
|
|
127
|
+
* produce a wire schema the supplied schema would reject.
|
|
128
|
+
*
|
|
129
|
+
* Opt-in because it is still a **semantic** change to the reply: the model must
|
|
130
|
+
* now emit `null` where it could previously omit the key. A caller that
|
|
131
|
+
* distinguishes those two — rather than treating them alike, as
|
|
132
|
+
* `optional(nullable)` says it does — should leave this off.
|
|
133
|
+
*
|
|
134
|
+
* Defaults to `false`. The enforcement report is unaffected: a schema that goes
|
|
135
|
+
* out reports `'schema'` whether or not any property was hoisted.
|
|
136
|
+
*/
|
|
137
|
+
readonly adaptOptionalToNullable?: boolean;
|
|
108
138
|
}
|
|
109
139
|
/**
|
|
110
140
|
* Ask the provider for syntactically valid JSON of arbitrary shape.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structuredOutputTypes.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMpD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAChC,oBAAoB,GACpB,yBAAyB,GACzB,wBAAwB,GACxB,uBAAuB,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1F;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"structuredOutputTypes.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMpD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAChC,oBAAoB,GACpB,yBAAyB,GACzB,wBAAwB,GACxB,uBAAuB,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1F;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC5C;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,CAAC;CACnD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,uBAAuB,GAAG,8BAA8B,GAAG,kCAAkC,CAAC"}
|
|
@@ -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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thinkingOptionsResolver.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/thinkingOptionsResolver.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAQ,MAAM,EAAW,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EACV,eAAe,EAEf,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAMjB;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEtF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,6BAA6B,GAAG,SAAS,CAaxG;AAMD;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC,mHAAmH;IACnH,QAAQ,CAAC,eAAe,CAAC,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC9D,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACxD,6DAA6D;IAC7D,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAClD,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;CACnC;
|
|
1
|
+
{"version":3,"file":"thinkingOptionsResolver.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/thinkingOptionsResolver.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAQ,MAAM,EAAW,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EACV,eAAe,EAEf,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAMjB;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEtF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,6BAA6B,GAAG,SAAS,CAaxG;AAMD;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC,mHAAmH;IACnH,QAAQ,CAAC,eAAe,CAAC,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC9D,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACxD,6DAA6D;IAC7D,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAClD,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;CACnC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,WAAW,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC,GACtD,MAAM,CAWR;AA6FD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,6BAA6B,GAC3C,MAAM,CAAC,uBAAuB,CAAC,CA+CjC;AAyDD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,uBAAuB,EACjC,aAAa,EAAE,6BAA6B,EAC5C,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,CAAC,SAAS,CAAC,CAkCnB"}
|
|
@@ -47,7 +47,10 @@ function providerDiscriminatorForId(providerId) {
|
|
|
47
47
|
// Common-subset mapping
|
|
48
48
|
// ============================================================================
|
|
49
49
|
/**
|
|
50
|
-
* Maps generic effort to Anthropic wire effort.
|
|
50
|
+
* Maps generic effort to Anthropic wire effort. Anthropic has no 'none' value in its
|
|
51
|
+
* own vocabulary — the caller (tier 1 of `mergeThinkingConfig`) handles `'none'` by
|
|
52
|
+
* skipping this mapping entirely, so `anthropicEffort` stays unset and the emit site
|
|
53
|
+
* omits the `thinking` wire param. @internal
|
|
51
54
|
*/
|
|
52
55
|
function genericEffortToAnthropic(effort) {
|
|
53
56
|
return effort; // 1:1 mapping for the common subset
|
|
@@ -82,10 +85,16 @@ function genericEffortToOpenAi(effort) {
|
|
|
82
85
|
return effort; // 1:1 mapping for the common subset
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
|
-
* Maps generic effort to Gemini thinkingBudget.
|
|
88
|
+
* Maps generic effort to Gemini thinkingBudget. `'none'` maps to `0` — Gemini's own
|
|
89
|
+
* off value on the wire, though `IGeminiThinkingConfig.thinkingBudget` documents that `0`
|
|
90
|
+
* is valid only on Flash/Flash-Lite and errors on Pro; this mapping is not model-aware and
|
|
91
|
+
* inherits that same caveat (not a new gap — an explicit `providers` block could already
|
|
92
|
+
* request `thinkingBudget: 0` on any model). @internal
|
|
86
93
|
*/
|
|
87
94
|
function genericEffortToGemini(effort) {
|
|
88
95
|
switch (effort) {
|
|
96
|
+
case 'none':
|
|
97
|
+
return 0;
|
|
89
98
|
case 'low':
|
|
90
99
|
return 1024;
|
|
91
100
|
case 'medium':
|
|
@@ -176,7 +185,13 @@ function mergeThinkingConfig(config, resolvedModel, discriminator) {
|
|
|
176
185
|
if (config.effort !== undefined) {
|
|
177
186
|
switch (discriminator) {
|
|
178
187
|
case 'anthropic':
|
|
179
|
-
|
|
188
|
+
// Anthropic has no 'none' value: "off" means no `thinking` wire param at all,
|
|
189
|
+
// so 'none' leaves `anthropicEffort` unset rather than mapping to a value. That
|
|
190
|
+
// also satisfies checkTemperatureConflict's anthropic gate (which fails only when
|
|
191
|
+
// anthropicEffort is set), so temperature survives without any special-casing there.
|
|
192
|
+
if (config.effort !== 'none') {
|
|
193
|
+
resolved = Object.assign(Object.assign({}, resolved), { anthropicEffort: genericEffortToAnthropic(config.effort) });
|
|
194
|
+
}
|
|
180
195
|
break;
|
|
181
196
|
case 'openai':
|
|
182
197
|
resolved = Object.assign(Object.assign({}, resolved), { openAiEffort: genericEffortToOpenAi(config.effort) });
|