@fgv/ts-extras 5.1.0-53 → 5.1.0-55
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/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/toolFormats.js +36 -0
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/ts-extras.d.ts +30 -0
- 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/toolFormats.d.ts +0 -23
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +36 -0
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/package.json +7 -7
|
@@ -130,6 +130,17 @@ function jsonObjectWire(format) {
|
|
|
130
130
|
* So this is treated as a **capability mismatch** and routed through the caller's
|
|
131
131
|
* existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly
|
|
132
132
|
* on request. Gemini and Anthropic have no such rule and are unaffected.
|
|
133
|
+
*
|
|
134
|
+
* **One narrow exception, and it does not weaken the above.** The first repair is
|
|
135
|
+
* unsafe *because the rewritten schema admits a reply the original rejects*. When
|
|
136
|
+
* the optional property's node **already admits `null`**, as it does when authored
|
|
137
|
+
* `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so
|
|
138
|
+
* listing the key in `required` only removes the model's option to omit it, and
|
|
139
|
+
* every reply the emitted schema permits still satisfies the supplied one. That
|
|
140
|
+
* case is hoisted by {@link hoistNullableOptionals} when the caller opts in via
|
|
141
|
+
* `adaptOptionalToNullable`, **and this function is then re-run on the result** —
|
|
142
|
+
* so a property that is genuinely not `null`-able still lands here and still
|
|
143
|
+
* refuses. The condition is read off the schema, never asserted by the caller.
|
|
133
144
|
* @internal
|
|
134
145
|
*/
|
|
135
146
|
export function hasOptionalProperties(raw) {
|
|
@@ -139,17 +150,68 @@ export function hasOptionalProperties(raw) {
|
|
|
139
150
|
if (raw === null || typeof raw !== 'object') {
|
|
140
151
|
return false;
|
|
141
152
|
}
|
|
142
|
-
const
|
|
143
|
-
const properties = node.properties;
|
|
153
|
+
const properties = raw.properties;
|
|
144
154
|
if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
|
|
145
|
-
const required = Array.isArray(
|
|
155
|
+
const required = Array.isArray(raw.required) ? raw.required : [];
|
|
146
156
|
for (const name of Object.keys(properties)) {
|
|
147
157
|
if (!required.includes(name)) {
|
|
148
158
|
return true;
|
|
149
159
|
}
|
|
150
160
|
}
|
|
151
161
|
}
|
|
152
|
-
return Object.values(
|
|
162
|
+
return Object.values(raw).some(hasOptionalProperties);
|
|
163
|
+
}
|
|
164
|
+
/** Whether a wire node's `type` admits `null` — either spelling. @internal */
|
|
165
|
+
function admitsNull(node) {
|
|
166
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return Array.isArray(node.type) && node.type.includes('null');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Rewrites `raw` so that every optional property whose node already admits `null`
|
|
173
|
+
* is listed in its parent's `required` array, at any depth.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* The rewrite is deliberately **narrow, and its narrowness is the safety argument.**
|
|
177
|
+
* `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored
|
|
178
|
+
* as `optional(string({ nullable: true }))` is already `['string', 'null']` on the
|
|
179
|
+
* wire and differs from its required sibling only by absence from `required`.
|
|
180
|
+
* Adding it there narrows the permitted replies from *absent-or-null-or-value* to
|
|
181
|
+
* *null-or-value* — a strict subset of what the caller's own schema accepts. No
|
|
182
|
+
* reply that satisfies the emitted schema can fail the supplied one.
|
|
183
|
+
*
|
|
184
|
+
* A property whose node does not admit `null` is left exactly as it was, which is
|
|
185
|
+
* what makes this composable with the existing guard rather than a replacement for
|
|
186
|
+
* it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable
|
|
187
|
+
* optional still routes through `onUnsupported`. **The verification is the original
|
|
188
|
+
* check, applied again** — there is no second notion of correctness to keep in sync.
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
export function hoistNullableOptionals(raw) {
|
|
192
|
+
if (Array.isArray(raw)) {
|
|
193
|
+
return raw.map(hoistNullableOptionals);
|
|
194
|
+
}
|
|
195
|
+
if (raw === null || typeof raw !== 'object') {
|
|
196
|
+
return raw;
|
|
197
|
+
}
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
200
|
+
out[key] = hoistNullableOptionals(value);
|
|
201
|
+
}
|
|
202
|
+
const properties = out.properties;
|
|
203
|
+
if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
|
|
204
|
+
const required = Array.isArray(out.required) ? [...out.required] : [];
|
|
205
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
206
|
+
if (!required.includes(name) && admitsNull(propSchema)) {
|
|
207
|
+
required.push(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (required.length > 0) {
|
|
211
|
+
out.required = required;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
153
215
|
}
|
|
154
216
|
/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */
|
|
155
217
|
function isOpenAiStrictFormat(format) {
|
|
@@ -237,13 +299,28 @@ export function resolveStructuredOutput(descriptor, model, request, serverTools,
|
|
|
237
299
|
let resolved;
|
|
238
300
|
let unsupported;
|
|
239
301
|
if (request.mode === 'schema') {
|
|
240
|
-
|
|
241
|
-
|
|
302
|
+
// Hoist BEFORE the guard, then let the guard judge the result. The rewrite only
|
|
303
|
+
// ever removes optionality that was safe to remove, so re-running the original
|
|
304
|
+
// check is the whole verification — a schema that still trips it was not
|
|
305
|
+
// adaptable, and refuses exactly as it did before the flag existed.
|
|
306
|
+
// Gated on the format, not just the flag: hoisting narrows what the model may
|
|
307
|
+
// send, so applying it where the all-required rule does not exist would change
|
|
308
|
+
// a reply on a provider that never needed it changed.
|
|
309
|
+
const strict = isOpenAiStrictFormat(format);
|
|
310
|
+
const adapt = strict && request.adaptOptionalToNullable === true;
|
|
311
|
+
const raw = adapt ? hoistNullableOptionals(request.schema.toJson()) : request.schema.toJson();
|
|
312
|
+
if (strict && hasOptionalProperties(raw)) {
|
|
242
313
|
// See `hasOptionalProperties` — a hard provider constraint, treated as a
|
|
243
314
|
// capability mismatch rather than relocated into an opaque 400.
|
|
244
315
|
unsupported =
|
|
245
316
|
`the supplied schema declares optional properties, and OpenAI strict structured output ` +
|
|
246
|
-
`requires every property to be required;
|
|
317
|
+
`requires every property to be required; ` +
|
|
318
|
+
(adapt
|
|
319
|
+
? `adaptOptionalToNullable hoisted the ones that admit null, but at least one does not — ` +
|
|
320
|
+
`author it as nullable (e.g. optional(string({ nullable: true }))) so null is an ` +
|
|
321
|
+
`accepted reply, make it required, or pass `
|
|
322
|
+
: `author them as required, adopt adaptOptionalToNullable if null is an accepted reply ` +
|
|
323
|
+
`for each of them, or pass `) +
|
|
247
324
|
`onUnsupported: 'degrade' to send the request unconstrained`;
|
|
248
325
|
}
|
|
249
326
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type { JsonObject, JsonValue } from '@fgv/ts-json-base';\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport type { AiServerToolConfig, IAiProviderDescriptor } from './model';\nimport type {\n IAiStructuredOutputCapability,\n StructuredOutputEnforcement,\n StructuredOutputFallback,\n StructuredOutputRequest\n} from './structuredOutputTypes';\nimport { toGeminiParameterSchema } from './toolFormats';\n\n/**\n * The name the Anthropic forced-tool path gives its synthetic tool.\n *\n * @remarks\n * Anthropic has no `response_format`; its structured-output mechanism is forced\n * tool use, so a tool must exist to be forced. The name is fgv-owned and never\n * reaches the caller — the structured-output resolver re-serializes the tool's\n * `input` back into `IAiCompletionResponse.content`, so a caller's converter sees\n * a JSON string exactly as it does on every other provider.\n * @public\n */\nexport const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME: string = 'fgv_structured_output';\n\n/**\n * A resolved structured-output decision: what will be enforced, and the wire\n * fields that enforce it.\n * @internal\n */\nexport interface IResolvedStructuredOutput {\n /** What to report on the response. */\n readonly enforcement: StructuredOutputEnforcement;\n /**\n * Fields to merge into the request, **at the location the format dictates** —\n * the request body for the OpenAI and Anthropic formats, `generationConfig` for\n * Gemini. Empty when `enforcement` is `'none'`.\n */\n readonly wire: JsonObject;\n}\n\n/** The `'none'` decision: nothing sent, nothing enforced. @internal */\nexport const NO_STRUCTURED_OUTPUT: IResolvedStructuredOutput = { enforcement: 'none', wire: {} };\n\n/**\n * Wire fields for a schema-constrained request. Every format can express this —\n * a structured-output capability that could not carry a schema would have nothing\n * to declare.\n * @internal\n */\nfunction schemaWire(\n format: IAiStructuredOutputCapability['format'],\n raw: JsonValue\n): IResolvedStructuredOutput {\n switch (format) {\n case 'openai-json-schema':\n return {\n enforcement: 'schema',\n wire: {\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'response', strict: true, schema: raw }\n }\n }\n };\n case 'openai-responses-format':\n // The Responses API nests the same choice under `text.format` and flattens the\n // schema onto the format object rather than a `json_schema` sub-object.\n return {\n enforcement: 'schema',\n wire: { text: { format: { type: 'json_schema', name: 'response', strict: true, schema: raw } } }\n };\n case 'gemini-response-schema':\n // Merged into `generationConfig`, not the body. Gemini's schema is an\n // OpenAPI-3.0 subset that REJECTS draft-07 keywords rather than ignoring them,\n // and `JsonSchema` is strict-by-default so `.toJson()` emits\n // `additionalProperties: false` on every object node — hence the same sanitizer\n // the tool path uses.\n return {\n enforcement: 'schema',\n wire: { responseMimeType: 'application/json', responseSchema: toGeminiParameterSchema(raw) }\n };\n case 'anthropic-tool-forced':\n // Anthropic has no response-format field. The schema becomes a synthetic\n // tool's `input_schema` and `tool_choice` forces it, which is why this is a\n // distinct enforcement value rather than a spelling of `'schema'`: the reply\n // arrives in a `tool_use` block, not as text.\n return {\n enforcement: 'tool-forced',\n wire: {\n tools: [\n {\n name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,\n description: 'Return the response as structured data matching the supplied schema.',\n input_schema: raw\n }\n ],\n tool_choice: { type: 'tool', name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME }\n }\n };\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Wire fields for a bare JSON-object request, or `undefined` when the format\n * cannot express one.\n *\n * @remarks\n * The `undefined` return **is** the capability table — there is deliberately no\n * separate `supportsJsonObject` flag anywhere, because a second declaration of\n * what a format can do could only ever disagree with this function.\n * @internal\n */\nfunction jsonObjectWire(\n format: IAiStructuredOutputCapability['format']\n): IResolvedStructuredOutput | undefined {\n switch (format) {\n case 'openai-json-schema':\n return { enforcement: 'json-mode', wire: { response_format: { type: 'json_object' } } };\n case 'openai-responses-format':\n return { enforcement: 'json-mode', wire: { text: { format: { type: 'json_object' } } } };\n case 'gemini-response-schema':\n return { enforcement: 'json-mode', wire: { responseMimeType: 'application/json' } };\n case 'anthropic-tool-forced':\n // A forced tool needs an input schema to be forced *to*, so there is no\n // schema-less form of this mechanism.\n return undefined;\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Whether `raw` declares any object property that is absent from that object's\n * `required` list — at any depth.\n *\n * @remarks\n * **This is a hard constraint of OpenAI's strict structured output, not a style\n * preference.** `response_format: { type: 'json_schema', json_schema: { strict: true } }`\n * requires *every* key in `properties` to appear in `required`; a schema that omits\n * one is rejected with a 400 before the model ever runs. `JsonSchema.optional(...)`\n * produces exactly that shape, so an authored schema with one optional field is\n * unsendable to the two OpenAI strict formats.\n *\n * The three obvious repairs are all worse than refusing. Rewriting optional to\n * required-and-nullable changes what the model must emit (`null` rather than\n * omission), so the reply would no longer satisfy the caller's own validator —\n * breaking the one-object-cannot-drift property this whole surface exists for.\n * Dropping `strict` silently downgrades the guarantee while still reporting\n * `'schema'`, which is the lie the required report exists to prevent. And sending\n * it anyway just relocates the failure to an opaque provider 400.\n *\n * So this is treated as a **capability mismatch** and routed through the caller's\n * existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly\n * on request. Gemini and Anthropic have no such rule and are unaffected.\n * @internal\n */\nexport function hasOptionalProperties(raw: JsonValue): boolean {\n if (Array.isArray(raw)) {\n return raw.some(hasOptionalProperties);\n }\n if (raw === null || typeof raw !== 'object') {\n return false;\n }\n const node: Record<string, JsonValue | undefined> = raw as Record<string, JsonValue | undefined>;\n const properties = node.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: ReadonlyArray<JsonValue> = Array.isArray(node.required) ? node.required : [];\n for (const name of Object.keys(properties)) {\n if (!required.includes(name)) {\n return true;\n }\n }\n }\n return Object.values(node).some((v) => v !== undefined && hasOptionalProperties(v));\n}\n\n/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */\nfunction isOpenAiStrictFormat(format: IAiStructuredOutputCapability['format']): boolean {\n return format === 'openai-json-schema' || format === 'openai-responses-format';\n}\n\n/**\n * Whether a resolved wire claims the provider's tools channel, and therefore\n * genuinely conflicts with server-side tools.\n *\n * @remarks\n * Asked of the **resolved wire** rather than the declared format, because a format\n * that *would* claim the channel does not claim it when the request degraded to\n * sending nothing. Anthropic + `json-object` is exactly that case: the mode has no\n * expression there, so the wire is empty and there is nothing to conflict with —\n * rejecting it would refuse a request that was about to become harmless.\n * @internal\n */\nfunction conflictsWithServerTools(\n format: IAiStructuredOutputCapability['format'],\n resolved: IResolvedStructuredOutput\n): boolean {\n return (\n resolved.enforcement !== 'none' &&\n (format === 'anthropic-tool-forced' || format === 'gemini-response-schema')\n );\n}\n\n/**\n * The wire format actually in force, given which OpenAI endpoint the dispatcher\n * will use.\n *\n * @remarks\n * **The OpenAI route is not a function of the model alone.** `callProviderCompletion`\n * sends a request to `/responses` when it carries server tools **or** when the model\n * is Responses-only, and to `/chat/completions` otherwise — so the same model takes\n * different endpoints on different calls, and those endpoints spell structured output\n * differently (`response_format` vs `text.format`). A capability declaration keyed on\n * the model therefore cannot name the right one by itself, and emitting\n * `response_format` into a `/responses` body would be silently ignored by the\n * provider: the request would look constrained and the reply would not be, with the\n * report confidently saying `'schema'`.\n *\n * The declaration still names each family's *support*; this is the one axis it cannot\n * carry, so it is supplied by the dispatcher that makes the routing decision.\n * @internal\n */\nfunction effectiveFormat(\n declared: IAiStructuredOutputCapability['format'],\n usesResponsesApi: boolean\n): IAiStructuredOutputCapability['format'] {\n if (usesResponsesApi && declared === 'openai-json-schema') {\n return 'openai-responses-format';\n }\n return declared;\n}\n\n/**\n * Resolve a caller's structured-output request against the concrete model that\n * will serve it.\n *\n * @param descriptor - The provider descriptor.\n * @param model - The **concrete** model id, already through `resolveProviderModel`.\n * Passing an alias here would be a bug of the class `resolveImageCapability` once\n * had, where an unresolved alias fell through to a catch-all `modelPrefix: ''` and\n * returned a confidently wrong capability.\n * @param request - The caller's intent, or `undefined` for no request at all.\n * @param serverTools - Server-side tools on the same request, which conflict with\n * structured output on two of the four formats.\n * @param usesResponsesApi - Whether the dispatcher will send this request to the\n * OpenAI Responses API rather than Chat Completions. See {@link effectiveFormat} —\n * the route is not a function of the model alone, so the capability declaration\n * cannot carry it.\n * @returns The decision, or `Failure` when the caller asked to fail rather than\n * degrade — or when the request conflicts with server tools, which is never\n * degradable because the caller asked for two things the provider cannot both do.\n * @internal\n */\nexport function resolveStructuredOutput(\n descriptor: IAiProviderDescriptor,\n model: string,\n request: StructuredOutputRequest | undefined,\n serverTools: ReadonlyArray<AiServerToolConfig> | undefined,\n usesResponsesApi: boolean,\n resolveCapability: (\n descriptor: IAiProviderDescriptor,\n model: string\n ) => IAiStructuredOutputCapability | undefined\n): Result<IResolvedStructuredOutput> {\n if (request === undefined) {\n return succeed(NO_STRUCTURED_OUTPUT);\n }\n const fallback: StructuredOutputFallback = request.onUnsupported ?? 'degrade';\n const capability = resolveCapability(descriptor, model);\n if (capability === undefined) {\n return fallback === 'fail'\n ? fail(\n `provider '${descriptor.id}' model '${model}' declares no structured-output capability; ` +\n `pass onUnsupported: 'degrade' to send the request unconstrained`\n )\n : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n const format = effectiveFormat(capability.format, usesResponsesApi);\n\n // Resolve the wire FIRST, then judge conflicts against what it actually is.\n // Ordering matters: a format that would claim the tools channel does not claim\n // it when the request degraded to sending nothing.\n let resolved: IResolvedStructuredOutput | undefined;\n let unsupported: string | undefined;\n if (request.mode === 'schema') {\n const raw: JsonValue = request.schema.toJson();\n if (isOpenAiStrictFormat(format) && hasOptionalProperties(raw)) {\n // See `hasOptionalProperties` — a hard provider constraint, treated as a\n // capability mismatch rather than relocated into an opaque 400.\n unsupported =\n `the supplied schema declares optional properties, and OpenAI strict structured output ` +\n `requires every property to be required; author them as required, or pass ` +\n `onUnsupported: 'degrade' to send the request unconstrained`;\n } else {\n resolved = schemaWire(format, raw);\n }\n } else {\n resolved = jsonObjectWire(format);\n if (resolved === undefined) {\n // Today this is only `'json-object'` on Anthropic, whose mechanism needs a\n // schema to force a tool to.\n unsupported = `provider '${descriptor.id}' model '${model}' cannot enforce '${request.mode}' structured output`;\n }\n }\n\n if (resolved === undefined) {\n return fallback === 'fail' ? fail(`${unsupported}`) : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n // Two formats cannot carry structured output and server-side tools at once, for\n // DIFFERENT reasons — worth separating, because a reader who assumes one\n // mechanism will reason wrongly about the other.\n //\n // anthropic-tool-forced: a wire-level clash. The constraint IS `tools` +\n // `tool_choice`, so server tools would be overwritten (and `tool_choice`\n // forces ours, which disables theirs anyway).\n // gemini-response-schema: NOT a wire clash — `responseMimeType` /\n // `responseSchema` live in `generationConfig`, nowhere near `tools`. It is\n // an API-level mutual exclusivity Gemini enforces, the same restriction the\n // client-tool path already pre-empts.\n //\n // Neither is degradable: silently dropping either half would give the caller\n // something they did not ask for, and `onUnsupported` speaks to what a model can\n // enforce, not to a caller asking for two incompatible things.\n if (serverTools !== undefined && serverTools.length > 0 && conflictsWithServerTools(format, resolved)) {\n const why =\n format === 'anthropic-tool-forced'\n ? 'Anthropic enforces structured output by forcing a tool, so it cannot be combined with'\n : 'Gemini cannot combine a response schema with';\n return fail(\n `${why} server-side tools (${serverTools.map((t) => t.type).join(', ')}) in the same request; ` +\n `send one or the other`\n );\n }\n\n return succeed(resolved);\n}\n\n/**\n * Every valid `StructuredOutputEnforcement`, for the wire-shape guard below.\n *\n * @remarks\n * A **total** `Record`, not a `Set` built from an array literal — the same reasoning\n * as `SCHEMA_NODE_TYPES` in `@fgv/ts-json-base`. A `Set` catches a removed or\n * misspelled member but not an *added* one, so a new enforcement value would compile\n * fine here while this guard silently began rejecting it off a proxy response. The\n * `Record` makes that addition a compile error at this line.\n * @internal\n */\nconst ENFORCEMENTS: Readonly<Record<StructuredOutputEnforcement, true>> = {\n none: true,\n 'json-mode': true,\n schema: true,\n 'tool-forced': true\n};\n\n/**\n * Whether an untyped value off a proxy response is a valid\n * `StructuredOutputEnforcement`.\n * @internal\n */\nexport function isStructuredOutputEnforcement(value: unknown): value is StructuredOutputEnforcement {\n // Indexed read compared to `true`, NOT `in` — `in` walks the prototype chain, so a\n // proxy answering `structuredOutput: 'constructor'` would pass it.\n return typeof value === 'string' && ENFORCEMENTS[value as StructuredOutputEnforcement] === true;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutput.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport type { JsonObject, JsonValue } from '@fgv/ts-json-base';\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport type { AiServerToolConfig, IAiProviderDescriptor } from './model';\nimport type {\n IAiStructuredOutputCapability,\n StructuredOutputEnforcement,\n StructuredOutputFallback,\n StructuredOutputRequest\n} from './structuredOutputTypes';\nimport { toGeminiParameterSchema } from './toolFormats';\n\n/**\n * The name the Anthropic forced-tool path gives its synthetic tool.\n *\n * @remarks\n * Anthropic has no `response_format`; its structured-output mechanism is forced\n * tool use, so a tool must exist to be forced. The name is fgv-owned and never\n * reaches the caller — the structured-output resolver re-serializes the tool's\n * `input` back into `IAiCompletionResponse.content`, so a caller's converter sees\n * a JSON string exactly as it does on every other provider.\n * @public\n */\nexport const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME: string = 'fgv_structured_output';\n\n/**\n * A resolved structured-output decision: what will be enforced, and the wire\n * fields that enforce it.\n * @internal\n */\nexport interface IResolvedStructuredOutput {\n /** What to report on the response. */\n readonly enforcement: StructuredOutputEnforcement;\n /**\n * Fields to merge into the request, **at the location the format dictates** —\n * the request body for the OpenAI and Anthropic formats, `generationConfig` for\n * Gemini. Empty when `enforcement` is `'none'`.\n */\n readonly wire: JsonObject;\n}\n\n/** The `'none'` decision: nothing sent, nothing enforced. @internal */\nexport const NO_STRUCTURED_OUTPUT: IResolvedStructuredOutput = { enforcement: 'none', wire: {} };\n\n/**\n * Wire fields for a schema-constrained request. Every format can express this —\n * a structured-output capability that could not carry a schema would have nothing\n * to declare.\n * @internal\n */\nfunction schemaWire(\n format: IAiStructuredOutputCapability['format'],\n raw: JsonValue\n): IResolvedStructuredOutput {\n switch (format) {\n case 'openai-json-schema':\n return {\n enforcement: 'schema',\n wire: {\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'response', strict: true, schema: raw }\n }\n }\n };\n case 'openai-responses-format':\n // The Responses API nests the same choice under `text.format` and flattens the\n // schema onto the format object rather than a `json_schema` sub-object.\n return {\n enforcement: 'schema',\n wire: { text: { format: { type: 'json_schema', name: 'response', strict: true, schema: raw } } }\n };\n case 'gemini-response-schema':\n // Merged into `generationConfig`, not the body. Gemini's schema is an\n // OpenAPI-3.0 subset that REJECTS draft-07 keywords rather than ignoring them,\n // and `JsonSchema` is strict-by-default so `.toJson()` emits\n // `additionalProperties: false` on every object node — hence the same sanitizer\n // the tool path uses.\n return {\n enforcement: 'schema',\n wire: { responseMimeType: 'application/json', responseSchema: toGeminiParameterSchema(raw) }\n };\n case 'anthropic-tool-forced':\n // Anthropic has no response-format field. The schema becomes a synthetic\n // tool's `input_schema` and `tool_choice` forces it, which is why this is a\n // distinct enforcement value rather than a spelling of `'schema'`: the reply\n // arrives in a `tool_use` block, not as text.\n return {\n enforcement: 'tool-forced',\n wire: {\n tools: [\n {\n name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,\n description: 'Return the response as structured data matching the supplied schema.',\n input_schema: raw\n }\n ],\n tool_choice: { type: 'tool', name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME }\n }\n };\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Wire fields for a bare JSON-object request, or `undefined` when the format\n * cannot express one.\n *\n * @remarks\n * The `undefined` return **is** the capability table — there is deliberately no\n * separate `supportsJsonObject` flag anywhere, because a second declaration of\n * what a format can do could only ever disagree with this function.\n * @internal\n */\nfunction jsonObjectWire(\n format: IAiStructuredOutputCapability['format']\n): IResolvedStructuredOutput | undefined {\n switch (format) {\n case 'openai-json-schema':\n return { enforcement: 'json-mode', wire: { response_format: { type: 'json_object' } } };\n case 'openai-responses-format':\n return { enforcement: 'json-mode', wire: { text: { format: { type: 'json_object' } } } };\n case 'gemini-response-schema':\n return { enforcement: 'json-mode', wire: { responseMimeType: 'application/json' } };\n case 'anthropic-tool-forced':\n // A forced tool needs an input schema to be forced *to*, so there is no\n // schema-less form of this mechanism.\n return undefined;\n /* c8 ignore next 4 - defensive: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = format;\n throw new Error(`unsupported structured-output format: ${String(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Whether `raw` declares any object property that is absent from that object's\n * `required` list — at any depth.\n *\n * @remarks\n * **This is a hard constraint of OpenAI's strict structured output, not a style\n * preference.** `response_format: { type: 'json_schema', json_schema: { strict: true } }`\n * requires *every* key in `properties` to appear in `required`; a schema that omits\n * one is rejected with a 400 before the model ever runs. `JsonSchema.optional(...)`\n * produces exactly that shape, so an authored schema with one optional field is\n * unsendable to the two OpenAI strict formats.\n *\n * The three obvious repairs are all worse than refusing. Rewriting optional to\n * required-and-nullable changes what the model must emit (`null` rather than\n * omission), so the reply would no longer satisfy the caller's own validator —\n * breaking the one-object-cannot-drift property this whole surface exists for.\n * Dropping `strict` silently downgrades the guarantee while still reporting\n * `'schema'`, which is the lie the required report exists to prevent. And sending\n * it anyway just relocates the failure to an opaque provider 400.\n *\n * So this is treated as a **capability mismatch** and routed through the caller's\n * existing `onUnsupported` choice — degrade to unconstrained by default, fail loudly\n * on request. Gemini and Anthropic have no such rule and are unaffected.\n *\n * **One narrow exception, and it does not weaken the above.** The first repair is\n * unsafe *because the rewritten schema admits a reply the original rejects*. When\n * the optional property's node **already admits `null`**, as it does when authored\n * `optional(string({ nullable: true }))`, that is not true of it: it accepts `null`, so\n * listing the key in `required` only removes the model's option to omit it, and\n * every reply the emitted schema permits still satisfies the supplied one. That\n * case is hoisted by {@link hoistNullableOptionals} when the caller opts in via\n * `adaptOptionalToNullable`, **and this function is then re-run on the result** —\n * so a property that is genuinely not `null`-able still lands here and still\n * refuses. The condition is read off the schema, never asserted by the caller.\n * @internal\n */\nexport function hasOptionalProperties(raw: JsonValue): boolean {\n if (Array.isArray(raw)) {\n return raw.some(hasOptionalProperties);\n }\n if (raw === null || typeof raw !== 'object') {\n return false;\n }\n const properties = raw.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: ReadonlyArray<JsonValue> = Array.isArray(raw.required) ? raw.required : [];\n for (const name of Object.keys(properties)) {\n if (!required.includes(name)) {\n return true;\n }\n }\n }\n return Object.values(raw).some(hasOptionalProperties);\n}\n\n/** Whether a wire node's `type` admits `null` — either spelling. @internal */\nfunction admitsNull(node: JsonValue): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n return false;\n }\n return Array.isArray(node.type) && node.type.includes('null');\n}\n\n/**\n * Rewrites `raw` so that every optional property whose node already admits `null`\n * is listed in its parent's `required` array, at any depth.\n *\n * @remarks\n * The rewrite is deliberately **narrow, and its narrowness is the safety argument.**\n * `JsonSchema.optional(...)` emits its inner node verbatim, so a property authored\n * as `optional(string({ nullable: true }))` is already `['string', 'null']` on the\n * wire and differs from its required sibling only by absence from `required`.\n * Adding it there narrows the permitted replies from *absent-or-null-or-value* to\n * *null-or-value* — a strict subset of what the caller's own schema accepts. No\n * reply that satisfies the emitted schema can fail the supplied one.\n *\n * A property whose node does not admit `null` is left exactly as it was, which is\n * what makes this composable with the existing guard rather than a replacement for\n * it: {@link hasOptionalProperties} is re-run on the output, so any non-hoistable\n * optional still routes through `onUnsupported`. **The verification is the original\n * check, applied again** — there is no second notion of correctness to keep in sync.\n * @internal\n */\nexport function hoistNullableOptionals(raw: JsonValue): JsonValue {\n if (Array.isArray(raw)) {\n return raw.map(hoistNullableOptionals);\n }\n if (raw === null || typeof raw !== 'object') {\n return raw;\n }\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(raw)) {\n out[key] = hoistNullableOptionals(value);\n }\n\n const properties = out.properties;\n if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {\n const required: JsonValue[] = Array.isArray(out.required) ? [...out.required] : [];\n for (const [name, propSchema] of Object.entries(properties)) {\n if (!required.includes(name) && admitsNull(propSchema)) {\n required.push(name);\n }\n }\n if (required.length > 0) {\n out.required = required;\n }\n }\n return out;\n}\n\n/** The two formats that carry OpenAI's all-properties-required strict rule. @internal */\nfunction isOpenAiStrictFormat(format: IAiStructuredOutputCapability['format']): boolean {\n return format === 'openai-json-schema' || format === 'openai-responses-format';\n}\n\n/**\n * Whether a resolved wire claims the provider's tools channel, and therefore\n * genuinely conflicts with server-side tools.\n *\n * @remarks\n * Asked of the **resolved wire** rather than the declared format, because a format\n * that *would* claim the channel does not claim it when the request degraded to\n * sending nothing. Anthropic + `json-object` is exactly that case: the mode has no\n * expression there, so the wire is empty and there is nothing to conflict with —\n * rejecting it would refuse a request that was about to become harmless.\n * @internal\n */\nfunction conflictsWithServerTools(\n format: IAiStructuredOutputCapability['format'],\n resolved: IResolvedStructuredOutput\n): boolean {\n return (\n resolved.enforcement !== 'none' &&\n (format === 'anthropic-tool-forced' || format === 'gemini-response-schema')\n );\n}\n\n/**\n * The wire format actually in force, given which OpenAI endpoint the dispatcher\n * will use.\n *\n * @remarks\n * **The OpenAI route is not a function of the model alone.** `callProviderCompletion`\n * sends a request to `/responses` when it carries server tools **or** when the model\n * is Responses-only, and to `/chat/completions` otherwise — so the same model takes\n * different endpoints on different calls, and those endpoints spell structured output\n * differently (`response_format` vs `text.format`). A capability declaration keyed on\n * the model therefore cannot name the right one by itself, and emitting\n * `response_format` into a `/responses` body would be silently ignored by the\n * provider: the request would look constrained and the reply would not be, with the\n * report confidently saying `'schema'`.\n *\n * The declaration still names each family's *support*; this is the one axis it cannot\n * carry, so it is supplied by the dispatcher that makes the routing decision.\n * @internal\n */\nfunction effectiveFormat(\n declared: IAiStructuredOutputCapability['format'],\n usesResponsesApi: boolean\n): IAiStructuredOutputCapability['format'] {\n if (usesResponsesApi && declared === 'openai-json-schema') {\n return 'openai-responses-format';\n }\n return declared;\n}\n\n/**\n * Resolve a caller's structured-output request against the concrete model that\n * will serve it.\n *\n * @param descriptor - The provider descriptor.\n * @param model - The **concrete** model id, already through `resolveProviderModel`.\n * Passing an alias here would be a bug of the class `resolveImageCapability` once\n * had, where an unresolved alias fell through to a catch-all `modelPrefix: ''` and\n * returned a confidently wrong capability.\n * @param request - The caller's intent, or `undefined` for no request at all.\n * @param serverTools - Server-side tools on the same request, which conflict with\n * structured output on two of the four formats.\n * @param usesResponsesApi - Whether the dispatcher will send this request to the\n * OpenAI Responses API rather than Chat Completions. See {@link effectiveFormat} —\n * the route is not a function of the model alone, so the capability declaration\n * cannot carry it.\n * @returns The decision, or `Failure` when the caller asked to fail rather than\n * degrade — or when the request conflicts with server tools, which is never\n * degradable because the caller asked for two things the provider cannot both do.\n * @internal\n */\nexport function resolveStructuredOutput(\n descriptor: IAiProviderDescriptor,\n model: string,\n request: StructuredOutputRequest | undefined,\n serverTools: ReadonlyArray<AiServerToolConfig> | undefined,\n usesResponsesApi: boolean,\n resolveCapability: (\n descriptor: IAiProviderDescriptor,\n model: string\n ) => IAiStructuredOutputCapability | undefined\n): Result<IResolvedStructuredOutput> {\n if (request === undefined) {\n return succeed(NO_STRUCTURED_OUTPUT);\n }\n const fallback: StructuredOutputFallback = request.onUnsupported ?? 'degrade';\n const capability = resolveCapability(descriptor, model);\n if (capability === undefined) {\n return fallback === 'fail'\n ? fail(\n `provider '${descriptor.id}' model '${model}' declares no structured-output capability; ` +\n `pass onUnsupported: 'degrade' to send the request unconstrained`\n )\n : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n const format = effectiveFormat(capability.format, usesResponsesApi);\n\n // Resolve the wire FIRST, then judge conflicts against what it actually is.\n // Ordering matters: a format that would claim the tools channel does not claim\n // it when the request degraded to sending nothing.\n let resolved: IResolvedStructuredOutput | undefined;\n let unsupported: string | undefined;\n if (request.mode === 'schema') {\n // Hoist BEFORE the guard, then let the guard judge the result. The rewrite only\n // ever removes optionality that was safe to remove, so re-running the original\n // check is the whole verification — a schema that still trips it was not\n // adaptable, and refuses exactly as it did before the flag existed.\n // Gated on the format, not just the flag: hoisting narrows what the model may\n // send, so applying it where the all-required rule does not exist would change\n // a reply on a provider that never needed it changed.\n const strict: boolean = isOpenAiStrictFormat(format);\n const adapt: boolean = strict && request.adaptOptionalToNullable === true;\n const raw: JsonValue = adapt ? hoistNullableOptionals(request.schema.toJson()) : request.schema.toJson();\n if (strict && hasOptionalProperties(raw)) {\n // See `hasOptionalProperties` — a hard provider constraint, treated as a\n // capability mismatch rather than relocated into an opaque 400.\n unsupported =\n `the supplied schema declares optional properties, and OpenAI strict structured output ` +\n `requires every property to be required; ` +\n (adapt\n ? `adaptOptionalToNullable hoisted the ones that admit null, but at least one does not — ` +\n `author it as nullable (e.g. optional(string({ nullable: true }))) so null is an ` +\n `accepted reply, make it required, or pass `\n : `author them as required, adopt adaptOptionalToNullable if null is an accepted reply ` +\n `for each of them, or pass `) +\n `onUnsupported: 'degrade' to send the request unconstrained`;\n } else {\n resolved = schemaWire(format, raw);\n }\n } else {\n resolved = jsonObjectWire(format);\n if (resolved === undefined) {\n // Today this is only `'json-object'` on Anthropic, whose mechanism needs a\n // schema to force a tool to.\n unsupported = `provider '${descriptor.id}' model '${model}' cannot enforce '${request.mode}' structured output`;\n }\n }\n\n if (resolved === undefined) {\n return fallback === 'fail' ? fail(`${unsupported}`) : succeed(NO_STRUCTURED_OUTPUT);\n }\n\n // Two formats cannot carry structured output and server-side tools at once, for\n // DIFFERENT reasons — worth separating, because a reader who assumes one\n // mechanism will reason wrongly about the other.\n //\n // anthropic-tool-forced: a wire-level clash. The constraint IS `tools` +\n // `tool_choice`, so server tools would be overwritten (and `tool_choice`\n // forces ours, which disables theirs anyway).\n // gemini-response-schema: NOT a wire clash — `responseMimeType` /\n // `responseSchema` live in `generationConfig`, nowhere near `tools`. It is\n // an API-level mutual exclusivity Gemini enforces, the same restriction the\n // client-tool path already pre-empts.\n //\n // Neither is degradable: silently dropping either half would give the caller\n // something they did not ask for, and `onUnsupported` speaks to what a model can\n // enforce, not to a caller asking for two incompatible things.\n if (serverTools !== undefined && serverTools.length > 0 && conflictsWithServerTools(format, resolved)) {\n const why =\n format === 'anthropic-tool-forced'\n ? 'Anthropic enforces structured output by forcing a tool, so it cannot be combined with'\n : 'Gemini cannot combine a response schema with';\n return fail(\n `${why} server-side tools (${serverTools.map((t) => t.type).join(', ')}) in the same request; ` +\n `send one or the other`\n );\n }\n\n return succeed(resolved);\n}\n\n/**\n * Every valid `StructuredOutputEnforcement`, for the wire-shape guard below.\n *\n * @remarks\n * A **total** `Record`, not a `Set` built from an array literal — the same reasoning\n * as `SCHEMA_NODE_TYPES` in `@fgv/ts-json-base`. A `Set` catches a removed or\n * misspelled member but not an *added* one, so a new enforcement value would compile\n * fine here while this guard silently began rejecting it off a proxy response. The\n * `Record` makes that addition a compile error at this line.\n * @internal\n */\nconst ENFORCEMENTS: Readonly<Record<StructuredOutputEnforcement, true>> = {\n none: true,\n 'json-mode': true,\n schema: true,\n 'tool-forced': true\n};\n\n/**\n * Whether an untyped value off a proxy response is a valid\n * `StructuredOutputEnforcement`.\n * @internal\n */\nexport function isStructuredOutputEnforcement(value: unknown): value is StructuredOutputEnforcement {\n // Indexed read compared to `true`, NOT `in` — `in` walks the prototype chain, so a\n // proxy answering `structuredOutput: 'constructor'` would pass it.\n return typeof value === 'string' && ENFORCEMENTS[value as StructuredOutputEnforcement] === true;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structuredOutputTypes.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Structured-output types: the capability a provider declares, the request a\n * caller makes, and the enforcement the response reports.\n *\n * @remarks\n * Their own module rather than part of `model.ts` because they depend on nothing\n * there — `model.ts` imports them, not the reverse — and because `model.ts` was\n * at the `max-lines` cap. A dependency-free cut is one of the few available at a\n * moment like that which is not chosen under pressure.\n * @packageDocumentation\n */\n\nimport { type JsonSchema } from '@fgv/ts-json-base';\n\n// ============================================================================\n// Structured output — capability\n// ============================================================================\n\n/**\n * Wire format a provider uses to express a structured-output constraint.\n *\n * @remarks\n * Four shapes, not one, and they differ in more than field names: the OpenAI\n * pair carry the schema in the request body, Gemini carries it inside\n * `generationConfig`, and Anthropic has no response-format field at all —\n * its mechanism is forced tool use, which is why `'tool-forced'` is a distinct\n * {@link AiAssist.StructuredOutputEnforcement} value rather than a spelling of\n * `'schema'`.\n * @public\n */\nexport type AiStructuredOutputFormat =\n | 'openai-json-schema'\n | 'openai-responses-format'\n | 'gemini-response-schema'\n | 'anthropic-tool-forced';\n\n/**\n * Structured-output capability for a model family within a provider. Used as an\n * entry in `IAiProviderDescriptor.structuredOutput`.\n *\n * @remarks\n * Deliberately thinner than its `imageGeneration` / `embedding` siblings: it\n * carries no `supportsX` flags, because what each format can enforce is a\n * property of the provider's **API surface** rather than of any one model, and a\n * per-entry declaration of it could only ever disagree with the one in code.\n * @public\n */\nexport interface IAiStructuredOutputCapability {\n /**\n * Prefix matched against the resolved completion model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes match a\n * model id, the longest prefix wins; ties are broken by first-encountered.\n */\n readonly modelPrefix: string;\n /** Wire format used to express the constraint for matching models. */\n readonly format: AiStructuredOutputFormat;\n}\n\n/**\n * Which constraint the provider was **asked** to apply to this response.\n *\n * @remarks\n * Three questions hide inside *\"did it honour my schema\"*, and they have different\n * owners:\n *\n * | question | answerable by |\n * |---|---|\n * | did we send a constraint? | this client, at request-build time |\n * | which constraint did the provider apply? | this client, from the resolved model's capability |\n * | does *this response* conform to my shape? | the caller's converter, and nothing else |\n *\n * This type answers the first two and deliberately not the third. Reporting\n * conformance would mean re-validating against the caller's own schema to\n * re-derive an answer the caller already holds.\n *\n * - `'none'` — nothing was sent; the resolved model declares no capability.\n * - `'json-mode'` — syntactically valid JSON is guaranteed; the shape is not.\n * - `'schema'` — generation was constrained to the supplied schema.\n * - `'tool-forced'` — Anthropic-style forced tool use; the shape comes from the\n * forced tool's input schema, and `content` is the re-serialized tool input.\n * @public\n */\nexport type StructuredOutputEnforcement = 'none' | 'json-mode' | 'schema' | 'tool-forced';\n\n/**\n * What to do when the resolved model cannot apply the requested constraint.\n *\n * @remarks\n * `'degrade'` is the default, and it is only safe **because\n * `IAiCompletionResponse.structuredOutput` is required** rather than\n * optional. Degrade-and-tell-me is safe; degrade-silently is the failure this\n * whole surface exists to remove — so the two decisions are one decision, not\n * two independent ones.\n *\n * Reach for `'fail'` when the output is persisted or put on a wire, where an\n * unconstrained generation that happens to parse is worse than an error because\n * it is wrong quietly. Leave it at `'degrade'` on paths that are *designed* to\n * degrade — an extractor that may return nothing, a segmenter that floors to a\n * mechanical chunker — where a hard failure would make this library less safe\n * than the code it replaces.\n * @public\n */\nexport type StructuredOutputFallback = 'degrade' | 'fail';\n\n/**\n * Ask the provider for JSON constrained to a schema.\n * @public\n */\nexport interface ISchemaStructuredOutputRequest {\n readonly mode: 'schema';\n /**\n * The schema to constrain generation to — **the same object you validate the\n * reply with**, so the wire schema and the check cannot drift.\n *\n * @remarks\n * Author it with `JsonSchema.object({...})` from `@fgv/ts-json-base`. This is\n * the property `@fgv/ts-extras-ollama`'s `chatStructured` already has; this\n * surface is its cloud sibling.\n */\n readonly schema: JsonSchema.ISchemaValidator<unknown>;\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * Ask the provider for syntactically valid JSON of arbitrary shape.\n *\n * @remarks\n * The weaker floor, and worth having on its own: the failure that motivated this\n * surface (`Expected ',' or '}' after property value` — an unescaped quote closing\n * a string early) is **syntactic**, so a JSON-mode guarantee removes it. Schema\n * constraint is what additionally buys shape. It is also the only mode some\n * model/provider pairs support.\n * @public\n */\nexport interface IJsonObjectStructuredOutputRequest {\n readonly mode: 'json-object';\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * A caller's structured-output intent.\n *\n * @remarks\n * A discriminated union rather than an optional `schema` whose absence means\n * *\"json-object please\"* — an absence that means something is the shape this repo\n * has been burned by (see `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, which\n * exists because a three-ways-ambiguous absence could not be read).\n *\n * **The caller supplies intent; the response reports outcome.** A request never\n * needs to know whether the constraint will be honoured, because\n * `resolveProviderModel` resolves aliases and tiers at *call* time — a `tier`\n * request can cascade — so the concrete model that will serve a request is not\n * knowable to the caller up front. Requiring it to know would be unsound, which\n * is why the report rides on the response rather than being a lookup.\n * @public\n */\nexport type StructuredOutputRequest = ISchemaStructuredOutputRequest | IJsonObjectStructuredOutputRequest;\n"]}
|
|
1
|
+
{"version":3,"file":"structuredOutputTypes.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/structuredOutputTypes.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Structured-output types: the capability a provider declares, the request a\n * caller makes, and the enforcement the response reports.\n *\n * @remarks\n * Their own module rather than part of `model.ts` because they depend on nothing\n * there — `model.ts` imports them, not the reverse — and because `model.ts` was\n * at the `max-lines` cap. A dependency-free cut is one of the few available at a\n * moment like that which is not chosen under pressure.\n * @packageDocumentation\n */\n\nimport { type JsonSchema } from '@fgv/ts-json-base';\n\n// ============================================================================\n// Structured output — capability\n// ============================================================================\n\n/**\n * Wire format a provider uses to express a structured-output constraint.\n *\n * @remarks\n * Four shapes, not one, and they differ in more than field names: the OpenAI\n * pair carry the schema in the request body, Gemini carries it inside\n * `generationConfig`, and Anthropic has no response-format field at all —\n * its mechanism is forced tool use, which is why `'tool-forced'` is a distinct\n * {@link AiAssist.StructuredOutputEnforcement} value rather than a spelling of\n * `'schema'`.\n * @public\n */\nexport type AiStructuredOutputFormat =\n | 'openai-json-schema'\n | 'openai-responses-format'\n | 'gemini-response-schema'\n | 'anthropic-tool-forced';\n\n/**\n * Structured-output capability for a model family within a provider. Used as an\n * entry in `IAiProviderDescriptor.structuredOutput`.\n *\n * @remarks\n * Deliberately thinner than its `imageGeneration` / `embedding` siblings: it\n * carries no `supportsX` flags, because what each format can enforce is a\n * property of the provider's **API surface** rather than of any one model, and a\n * per-entry declaration of it could only ever disagree with the one in code.\n * @public\n */\nexport interface IAiStructuredOutputCapability {\n /**\n * Prefix matched against the resolved completion model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes match a\n * model id, the longest prefix wins; ties are broken by first-encountered.\n */\n readonly modelPrefix: string;\n /** Wire format used to express the constraint for matching models. */\n readonly format: AiStructuredOutputFormat;\n}\n\n/**\n * Which constraint the provider was **asked** to apply to this response.\n *\n * @remarks\n * Three questions hide inside *\"did it honour my schema\"*, and they have different\n * owners:\n *\n * | question | answerable by |\n * |---|---|\n * | did we send a constraint? | this client, at request-build time |\n * | which constraint did the provider apply? | this client, from the resolved model's capability |\n * | does *this response* conform to my shape? | the caller's converter, and nothing else |\n *\n * This type answers the first two and deliberately not the third. Reporting\n * conformance would mean re-validating against the caller's own schema to\n * re-derive an answer the caller already holds.\n *\n * - `'none'` — nothing was sent; the resolved model declares no capability.\n * - `'json-mode'` — syntactically valid JSON is guaranteed; the shape is not.\n * - `'schema'` — generation was constrained to the supplied schema.\n * - `'tool-forced'` — Anthropic-style forced tool use; the shape comes from the\n * forced tool's input schema, and `content` is the re-serialized tool input.\n * @public\n */\nexport type StructuredOutputEnforcement = 'none' | 'json-mode' | 'schema' | 'tool-forced';\n\n/**\n * What to do when the resolved model cannot apply the requested constraint.\n *\n * @remarks\n * `'degrade'` is the default, and it is only safe **because\n * `IAiCompletionResponse.structuredOutput` is required** rather than\n * optional. Degrade-and-tell-me is safe; degrade-silently is the failure this\n * whole surface exists to remove — so the two decisions are one decision, not\n * two independent ones.\n *\n * Reach for `'fail'` when the output is persisted or put on a wire, where an\n * unconstrained generation that happens to parse is worse than an error because\n * it is wrong quietly. Leave it at `'degrade'` on paths that are *designed* to\n * degrade — an extractor that may return nothing, a segmenter that floors to a\n * mechanical chunker — where a hard failure would make this library less safe\n * than the code it replaces.\n * @public\n */\nexport type StructuredOutputFallback = 'degrade' | 'fail';\n\n/**\n * Ask the provider for JSON constrained to a schema.\n * @public\n */\nexport interface ISchemaStructuredOutputRequest {\n readonly mode: 'schema';\n /**\n * The schema to constrain generation to — **the same object you validate the\n * reply with**, so the wire schema and the check cannot drift.\n *\n * @remarks\n * Author it with `JsonSchema.object({...})` from `@fgv/ts-json-base`. This is\n * the property `@fgv/ts-extras-ollama`'s `chatStructured` already has; this\n * surface is its cloud sibling.\n */\n readonly schema: JsonSchema.ISchemaValidator<unknown>;\n readonly onUnsupported?: StructuredOutputFallback;\n /**\n * On a format that requires every property to be `required`, send an optional\n * property as required when its node **already admits `null`**, instead of\n * refusing the whole schema.\n *\n * @remarks\n * `JsonSchema.optional(...)` emits its inner node verbatim — optionality lives\n * only in the parent's `required` array — so for a property authored as\n * `optional(string({ nullable: true }))` the wire node is already\n * `['string', 'null']`, and adding the key to `required` narrows what the model\n * may send from *absent-or-null-or-value* to *null-or-value*. **Every reply the\n * narrowed schema permits, the original schema already accepted**, so the\n * one-object-cannot-drift property is preserved rather than traded away.\n *\n * That is why this is not the caller asserting its validator tolerates `null`.\n * An assertion could be false; this is read off the schema. A property authored\n * as plain `optional(string())` rejects `null`, is therefore **not** hoistable,\n * and the schema still refuses through `onUnsupported` exactly as before — with\n * an error naming the properties that blocked it. Setting this flag can never\n * produce a wire schema the supplied schema would reject.\n *\n * Opt-in because it is still a **semantic** change to the reply: the model must\n * now emit `null` where it could previously omit the key. A caller that\n * distinguishes those two — rather than treating them alike, as\n * `optional(nullable)` says it does — should leave this off.\n *\n * Defaults to `false`. The enforcement report is unaffected: a schema that goes\n * out reports `'schema'` whether or not any property was hoisted.\n */\n readonly adaptOptionalToNullable?: boolean;\n}\n\n/**\n * Ask the provider for syntactically valid JSON of arbitrary shape.\n *\n * @remarks\n * The weaker floor, and worth having on its own: the failure that motivated this\n * surface (`Expected ',' or '}' after property value` — an unescaped quote closing\n * a string early) is **syntactic**, so a JSON-mode guarantee removes it. Schema\n * constraint is what additionally buys shape. It is also the only mode some\n * model/provider pairs support.\n * @public\n */\nexport interface IJsonObjectStructuredOutputRequest {\n readonly mode: 'json-object';\n readonly onUnsupported?: StructuredOutputFallback;\n}\n\n/**\n * A caller's structured-output intent.\n *\n * @remarks\n * A discriminated union rather than an optional `schema` whose absence means\n * *\"json-object please\"* — an absence that means something is the shape this repo\n * has been burned by (see `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, which\n * exists because a three-ways-ambiguous absence could not be read).\n *\n * **The caller supplies intent; the response reports outcome.** A request never\n * needs to know whether the constraint will be honoured, because\n * `resolveProviderModel` resolves aliases and tiers at *call* time — a `tier`\n * request can cascade — so the concrete model that will serve a request is not\n * knowable to the caller up front. Requiring it to know would be unsound, which\n * is why the report rides on the response rather than being a lookup.\n * @public\n */\nexport type StructuredOutputRequest = ISchemaStructuredOutputRequest | IJsonObjectStructuredOutputRequest;\n"]}
|
|
@@ -185,16 +185,52 @@ export function toAnthropicTools(tools) {
|
|
|
185
185
|
*
|
|
186
186
|
* @internal
|
|
187
187
|
*/
|
|
188
|
+
/**
|
|
189
|
+
* The non-`null` member of a draft-07 nullable `type` union, or `undefined` when `type` is
|
|
190
|
+
* not one.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* Deliberately narrow: only the two-member `[<type>, 'null']` shape `JsonSchema` emits is
|
|
194
|
+
* recognised. A general union has no OpenAPI equivalent, so translating one would be
|
|
195
|
+
* inventing a meaning — it is passed through unchanged and Gemini refuses it, which is the
|
|
196
|
+
* honest outcome.
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
function _nullableUnionMember(type) {
|
|
200
|
+
if (!Array.isArray(type) || type.length !== 2 || !type.includes('null')) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const other = type.find((member) => member !== 'null');
|
|
204
|
+
return typeof other === 'string' ? other : undefined;
|
|
205
|
+
}
|
|
188
206
|
export function toGeminiParameterSchema(schema) {
|
|
189
207
|
if (Array.isArray(schema)) {
|
|
190
208
|
return schema.map(toGeminiParameterSchema);
|
|
191
209
|
}
|
|
192
210
|
if (schema !== null && typeof schema === 'object') {
|
|
193
211
|
const out = {};
|
|
212
|
+
// Nullability is spelled differently in the two dialects and they are mutually
|
|
213
|
+
// exclusive: draft-07 (and OpenAI strict mode) wants `type: ['string', 'null']`,
|
|
214
|
+
// OpenAPI 3.0 (and Gemini) wants `type: 'string'` + `nullable: true` and rejects the
|
|
215
|
+
// union array. This is the same class of translation as the `additionalProperties`
|
|
216
|
+
// strip above — a dialect difference the consumer should not have to know about.
|
|
217
|
+
const nullableType = _nullableUnionMember(schema.type);
|
|
194
218
|
for (const [key, value] of Object.entries(schema)) {
|
|
195
219
|
if (key === 'additionalProperties' || key === '$schema') {
|
|
196
220
|
continue;
|
|
197
221
|
}
|
|
222
|
+
if (nullableType !== undefined && key === 'type') {
|
|
223
|
+
out.type = nullableType;
|
|
224
|
+
out.nullable = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (nullableType !== undefined && key === 'enum' && Array.isArray(value)) {
|
|
228
|
+
// A nullable enum carries `null` among its values in draft-07. OpenAPI expresses
|
|
229
|
+
// that with `nullable` alone, so the member is dropped rather than sent as a value
|
|
230
|
+
// Gemini would reject.
|
|
231
|
+
out.enum = value.filter((member) => member !== null);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
198
234
|
if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
199
235
|
// `properties` maps user-defined parameter names to subschemas: recurse each
|
|
200
236
|
// subschema value but never treat a parameter name as a strippable keyword.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolFormats.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.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 * Provider-specific tool format translation and tool resolution logic.\n * @packageDocumentation\n */\n\nimport { type JsonObject, type JsonValue } from '@fgv/ts-json-base';\n\nimport {\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiClientToolConfig,\n type IAiProviderDescriptor,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig\n} from './model';\n\n// ============================================================================\n// Tool resolution\n// ============================================================================\n\n/**\n * Resolves the effective tools for a completion call.\n *\n * - If per-call tools are provided, they override settings-level tools entirely.\n * - Otherwise, settings-level enabled tools are used.\n * - Only tools supported by the provider are included.\n * - Returns an empty array if no tools are enabled (= no tools sent).\n *\n * @param descriptor - The provider descriptor (used to filter by supported tools)\n * @param settingsTools - Tool enablement from provider settings (optional)\n * @param perCallTools - Per-call tool override (optional)\n * @returns The resolved list of tool configs to include in the request\n * @public\n */\nexport function resolveEffectiveTools(\n descriptor: IAiProviderDescriptor,\n settingsTools?: ReadonlyArray<IAiToolEnablement>,\n perCallTools?: ReadonlyArray<AiServerToolConfig>\n): ReadonlyArray<AiServerToolConfig> {\n const supported = new Set(descriptor.supportedTools);\n\n if (perCallTools !== undefined) {\n return perCallTools.filter((t) => supported.has(t.type));\n }\n\n if (settingsTools === undefined) {\n return [];\n }\n\n return settingsTools\n .filter((e) => e.enabled && supported.has(e.type))\n .map((e): AiServerToolConfig => e.config ?? { type: e.type });\n}\n\n// ============================================================================\n// OpenAI / xAI Responses API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction webSearchToResponsesApi(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = { type: 'web_search' };\n\n if (config.allowedDomains || config.blockedDomains) {\n const filters: Record<string, unknown> = {};\n if (config.allowedDomains) {\n filters.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n filters.excluded_domains = [...config.blockedDomains];\n }\n tool.filters = filters;\n }\n\n if (config.enableImageUnderstanding) {\n tool.enable_image_understanding = true;\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction clientToolToResponsesApi(config: IAiClientToolConfig): JsonObject {\n return {\n type: 'function',\n name: config.name,\n description: config.description,\n parameters: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the xAI/OpenAI Responses API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toResponsesApiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToResponsesApi(t);\n case 'client_tool':\n return clientToolToResponsesApi(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Anthropic Messages API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the Anthropic Messages API.\n * @internal\n */\nfunction webSearchToAnthropic(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = {\n type: 'web_search_20250305',\n name: 'web_search'\n };\n\n if (config.maxUses !== undefined) {\n tool.max_uses = config.maxUses;\n }\n if (config.allowedDomains) {\n tool.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n tool.blocked_domains = [...config.blockedDomains];\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the Anthropic Messages API.\n * Note: Anthropic client tools have no `type` field (unlike server tools).\n * @internal\n */\nfunction clientToolToAnthropic(config: IAiClientToolConfig): JsonObject {\n return {\n name: config.name,\n description: config.description,\n input_schema: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the Anthropic Messages API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toAnthropicTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToAnthropic(t);\n case 'client_tool':\n return clientToolToAnthropic(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Gemini generateContent API format\n// ============================================================================\n\n/**\n * Sanitizes a draft-07 JSON Schema (as emitted by `JsonSchema.object(...).toJson()`)\n * into the OpenAPI 3.0 Schema Object subset that Gemini's `function_declarations[].parameters`\n * accepts.\n *\n * @remarks\n * Gemini's function-declaration schema is **not** full JSON Schema — it is a subset of\n * the OpenAPI 3.0 Schema Object and **rejects** (rather than ignores) draft-07-only\n * keywords. `JsonSchema` objects are strict-by-default, so `.toJson()` emits\n * `additionalProperties: false` on every object node, which 400s the whole request on\n * Gemini. This helper recursively strips the unsupported keywords so any\n * `JsonSchema`-authored client tool works on Gemini without consumer awareness of the\n * dialect difference. Stripping is infallible, so it returns a plain value rather than a\n * `Result`.\n *\n * `additionalProperties` and `$schema` are stripped only where they appear as schema\n * *keywords* (siblings of `type`/`properties`/etc.). Inside a `properties` map the keys\n * are user-defined parameter names, not keywords, so they are preserved verbatim while\n * each property's subschema value is still recursively sanitized — a tool parameter\n * legitimately named `additionalProperties` survives.\n *\n * @internal\n */\nexport function toGeminiParameterSchema(schema: JsonValue): JsonValue {\n if (Array.isArray(schema)) {\n return schema.map(toGeminiParameterSchema);\n }\n if (schema !== null && typeof schema === 'object') {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(schema)) {\n if (key === 'additionalProperties' || key === '$schema') {\n continue;\n }\n if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n // `properties` maps user-defined parameter names to subschemas: recurse each\n // subschema value but never treat a parameter name as a strippable keyword.\n const properties: JsonObject = {};\n for (const [name, propSchema] of Object.entries(value)) {\n properties[name] = toGeminiParameterSchema(propSchema);\n }\n out[key] = properties;\n } else {\n out[key] = toGeminiParameterSchema(value);\n }\n }\n return out;\n }\n return schema;\n}\n\n/**\n * Formats tool configs for the Gemini generateContent API.\n *\n * @remarks\n * Gemini uses `google_search` for search grounding (no per-tool config).\n * Client-defined tools are accumulated into a single `function_declarations` entry.\n * Each client tool's parameters schema is sanitized to Gemini's OpenAPI-subset\n * dialect via {@link toGeminiParameterSchema} (the raw draft-07 `.toJson()` output\n * carries `additionalProperties`, which Gemini rejects).\n *\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toGeminiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n const result: JsonObject[] = [];\n const functionDeclarations: JsonObject[] = [];\n\n for (const t of tools) {\n switch (t.type) {\n case 'web_search':\n result.push({ google_search: {} } as JsonObject);\n break;\n case 'client_tool':\n functionDeclarations.push({\n name: t.name,\n description: t.description,\n parameters: toGeminiParameterSchema(t.parametersSchema.toJson())\n } as JsonObject);\n break;\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n result.push({ type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject);\n }\n }\n }\n\n if (functionDeclarations.length > 0) {\n result.push({ function_declarations: functionDeclarations } as JsonObject);\n }\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"toolFormats.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.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 * Provider-specific tool format translation and tool resolution logic.\n * @packageDocumentation\n */\n\nimport { type JsonObject, type JsonValue } from '@fgv/ts-json-base';\n\nimport {\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiClientToolConfig,\n type IAiProviderDescriptor,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig\n} from './model';\n\n// ============================================================================\n// Tool resolution\n// ============================================================================\n\n/**\n * Resolves the effective tools for a completion call.\n *\n * - If per-call tools are provided, they override settings-level tools entirely.\n * - Otherwise, settings-level enabled tools are used.\n * - Only tools supported by the provider are included.\n * - Returns an empty array if no tools are enabled (= no tools sent).\n *\n * @param descriptor - The provider descriptor (used to filter by supported tools)\n * @param settingsTools - Tool enablement from provider settings (optional)\n * @param perCallTools - Per-call tool override (optional)\n * @returns The resolved list of tool configs to include in the request\n * @public\n */\nexport function resolveEffectiveTools(\n descriptor: IAiProviderDescriptor,\n settingsTools?: ReadonlyArray<IAiToolEnablement>,\n perCallTools?: ReadonlyArray<AiServerToolConfig>\n): ReadonlyArray<AiServerToolConfig> {\n const supported = new Set(descriptor.supportedTools);\n\n if (perCallTools !== undefined) {\n return perCallTools.filter((t) => supported.has(t.type));\n }\n\n if (settingsTools === undefined) {\n return [];\n }\n\n return settingsTools\n .filter((e) => e.enabled && supported.has(e.type))\n .map((e): AiServerToolConfig => e.config ?? { type: e.type });\n}\n\n// ============================================================================\n// OpenAI / xAI Responses API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction webSearchToResponsesApi(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = { type: 'web_search' };\n\n if (config.allowedDomains || config.blockedDomains) {\n const filters: Record<string, unknown> = {};\n if (config.allowedDomains) {\n filters.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n filters.excluded_domains = [...config.blockedDomains];\n }\n tool.filters = filters;\n }\n\n if (config.enableImageUnderstanding) {\n tool.enable_image_understanding = true;\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction clientToolToResponsesApi(config: IAiClientToolConfig): JsonObject {\n return {\n type: 'function',\n name: config.name,\n description: config.description,\n parameters: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the xAI/OpenAI Responses API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toResponsesApiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToResponsesApi(t);\n case 'client_tool':\n return clientToolToResponsesApi(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Anthropic Messages API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the Anthropic Messages API.\n * @internal\n */\nfunction webSearchToAnthropic(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = {\n type: 'web_search_20250305',\n name: 'web_search'\n };\n\n if (config.maxUses !== undefined) {\n tool.max_uses = config.maxUses;\n }\n if (config.allowedDomains) {\n tool.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n tool.blocked_domains = [...config.blockedDomains];\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the Anthropic Messages API.\n * Note: Anthropic client tools have no `type` field (unlike server tools).\n * @internal\n */\nfunction clientToolToAnthropic(config: IAiClientToolConfig): JsonObject {\n return {\n name: config.name,\n description: config.description,\n input_schema: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the Anthropic Messages API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toAnthropicTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToAnthropic(t);\n case 'client_tool':\n return clientToolToAnthropic(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Gemini generateContent API format\n// ============================================================================\n\n/**\n * Sanitizes a draft-07 JSON Schema (as emitted by `JsonSchema.object(...).toJson()`)\n * into the OpenAPI 3.0 Schema Object subset that Gemini's `function_declarations[].parameters`\n * accepts.\n *\n * @remarks\n * Gemini's function-declaration schema is **not** full JSON Schema — it is a subset of\n * the OpenAPI 3.0 Schema Object and **rejects** (rather than ignores) draft-07-only\n * keywords. `JsonSchema` objects are strict-by-default, so `.toJson()` emits\n * `additionalProperties: false` on every object node, which 400s the whole request on\n * Gemini. This helper recursively strips the unsupported keywords so any\n * `JsonSchema`-authored client tool works on Gemini without consumer awareness of the\n * dialect difference. Stripping is infallible, so it returns a plain value rather than a\n * `Result`.\n *\n * `additionalProperties` and `$schema` are stripped only where they appear as schema\n * *keywords* (siblings of `type`/`properties`/etc.). Inside a `properties` map the keys\n * are user-defined parameter names, not keywords, so they are preserved verbatim while\n * each property's subschema value is still recursively sanitized — a tool parameter\n * legitimately named `additionalProperties` survives.\n *\n * @internal\n */\n/**\n * The non-`null` member of a draft-07 nullable `type` union, or `undefined` when `type` is\n * not one.\n *\n * @remarks\n * Deliberately narrow: only the two-member `[<type>, 'null']` shape `JsonSchema` emits is\n * recognised. A general union has no OpenAPI equivalent, so translating one would be\n * inventing a meaning — it is passed through unchanged and Gemini refuses it, which is the\n * honest outcome.\n * @internal\n */\nfunction _nullableUnionMember(type: JsonValue | undefined): string | undefined {\n if (!Array.isArray(type) || type.length !== 2 || !type.includes('null')) {\n return undefined;\n }\n const other: JsonValue | undefined = type.find((member) => member !== 'null');\n return typeof other === 'string' ? other : undefined;\n}\n\nexport function toGeminiParameterSchema(schema: JsonValue): JsonValue {\n if (Array.isArray(schema)) {\n return schema.map(toGeminiParameterSchema);\n }\n if (schema !== null && typeof schema === 'object') {\n const out: JsonObject = {};\n // Nullability is spelled differently in the two dialects and they are mutually\n // exclusive: draft-07 (and OpenAI strict mode) wants `type: ['string', 'null']`,\n // OpenAPI 3.0 (and Gemini) wants `type: 'string'` + `nullable: true` and rejects the\n // union array. This is the same class of translation as the `additionalProperties`\n // strip above — a dialect difference the consumer should not have to know about.\n const nullableType: string | undefined = _nullableUnionMember(schema.type);\n for (const [key, value] of Object.entries(schema)) {\n if (key === 'additionalProperties' || key === '$schema') {\n continue;\n }\n if (nullableType !== undefined && key === 'type') {\n out.type = nullableType;\n out.nullable = true;\n continue;\n }\n if (nullableType !== undefined && key === 'enum' && Array.isArray(value)) {\n // A nullable enum carries `null` among its values in draft-07. OpenAPI expresses\n // that with `nullable` alone, so the member is dropped rather than sent as a value\n // Gemini would reject.\n out.enum = value.filter((member) => member !== null);\n continue;\n }\n if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n // `properties` maps user-defined parameter names to subschemas: recurse each\n // subschema value but never treat a parameter name as a strippable keyword.\n const properties: JsonObject = {};\n for (const [name, propSchema] of Object.entries(value)) {\n properties[name] = toGeminiParameterSchema(propSchema);\n }\n out[key] = properties;\n } else {\n out[key] = toGeminiParameterSchema(value);\n }\n }\n return out;\n }\n return schema;\n}\n\n/**\n * Formats tool configs for the Gemini generateContent API.\n *\n * @remarks\n * Gemini uses `google_search` for search grounding (no per-tool config).\n * Client-defined tools are accumulated into a single `function_declarations` entry.\n * Each client tool's parameters schema is sanitized to Gemini's OpenAPI-subset\n * dialect via {@link toGeminiParameterSchema} (the raw draft-07 `.toJson()` output\n * carries `additionalProperties`, which Gemini rejects).\n *\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toGeminiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n const result: JsonObject[] = [];\n const functionDeclarations: JsonObject[] = [];\n\n for (const t of tools) {\n switch (t.type) {\n case 'web_search':\n result.push({ google_search: {} } as JsonObject);\n break;\n case 'client_tool':\n functionDeclarations.push({\n name: t.name,\n description: t.description,\n parameters: toGeminiParameterSchema(t.parametersSchema.toJson())\n } as JsonObject);\n break;\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n result.push({ type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject);\n }\n }\n }\n\n if (functionDeclarations.length > 0) {\n result.push({ function_declarations: functionDeclarations } as JsonObject);\n }\n\n return result;\n}\n"]}
|
package/dist/ts-extras.d.ts
CHANGED
|
@@ -5959,6 +5959,36 @@ declare interface ISchemaStructuredOutputRequest {
|
|
|
5959
5959
|
*/
|
|
5960
5960
|
readonly schema: JsonSchema.ISchemaValidator<unknown>;
|
|
5961
5961
|
readonly onUnsupported?: StructuredOutputFallback;
|
|
5962
|
+
/**
|
|
5963
|
+
* On a format that requires every property to be `required`, send an optional
|
|
5964
|
+
* property as required when its node **already admits `null`**, instead of
|
|
5965
|
+
* refusing the whole schema.
|
|
5966
|
+
*
|
|
5967
|
+
* @remarks
|
|
5968
|
+
* `JsonSchema.optional(...)` emits its inner node verbatim — optionality lives
|
|
5969
|
+
* only in the parent's `required` array — so for a property authored as
|
|
5970
|
+
* `optional(string({ nullable: true }))` the wire node is already
|
|
5971
|
+
* `['string', 'null']`, and adding the key to `required` narrows what the model
|
|
5972
|
+
* may send from *absent-or-null-or-value* to *null-or-value*. **Every reply the
|
|
5973
|
+
* narrowed schema permits, the original schema already accepted**, so the
|
|
5974
|
+
* one-object-cannot-drift property is preserved rather than traded away.
|
|
5975
|
+
*
|
|
5976
|
+
* That is why this is not the caller asserting its validator tolerates `null`.
|
|
5977
|
+
* An assertion could be false; this is read off the schema. A property authored
|
|
5978
|
+
* as plain `optional(string())` rejects `null`, is therefore **not** hoistable,
|
|
5979
|
+
* and the schema still refuses through `onUnsupported` exactly as before — with
|
|
5980
|
+
* an error naming the properties that blocked it. Setting this flag can never
|
|
5981
|
+
* produce a wire schema the supplied schema would reject.
|
|
5982
|
+
*
|
|
5983
|
+
* Opt-in because it is still a **semantic** change to the reply: the model must
|
|
5984
|
+
* now emit `null` where it could previously omit the key. A caller that
|
|
5985
|
+
* distinguishes those two — rather than treating them alike, as
|
|
5986
|
+
* `optional(nullable)` says it does — should leave this off.
|
|
5987
|
+
*
|
|
5988
|
+
* Defaults to `false`. The enforcement report is unaffected: a schema that goes
|
|
5989
|
+
* out reports `'schema'` whether or not any property was hoisted.
|
|
5990
|
+
*/
|
|
5991
|
+
readonly adaptOptionalToNullable?: boolean;
|
|
5962
5992
|
}
|
|
5963
5993
|
|
|
5964
5994
|
/**
|
|
@@ -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"]}
|
|
@@ -33,29 +33,6 @@ export declare function toResponsesApiTools(tools: ReadonlyArray<AiToolConfig>):
|
|
|
33
33
|
* @public
|
|
34
34
|
*/
|
|
35
35
|
export declare function toAnthropicTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject>;
|
|
36
|
-
/**
|
|
37
|
-
* Sanitizes a draft-07 JSON Schema (as emitted by `JsonSchema.object(...).toJson()`)
|
|
38
|
-
* into the OpenAPI 3.0 Schema Object subset that Gemini's `function_declarations[].parameters`
|
|
39
|
-
* accepts.
|
|
40
|
-
*
|
|
41
|
-
* @remarks
|
|
42
|
-
* Gemini's function-declaration schema is **not** full JSON Schema — it is a subset of
|
|
43
|
-
* the OpenAPI 3.0 Schema Object and **rejects** (rather than ignores) draft-07-only
|
|
44
|
-
* keywords. `JsonSchema` objects are strict-by-default, so `.toJson()` emits
|
|
45
|
-
* `additionalProperties: false` on every object node, which 400s the whole request on
|
|
46
|
-
* Gemini. This helper recursively strips the unsupported keywords so any
|
|
47
|
-
* `JsonSchema`-authored client tool works on Gemini without consumer awareness of the
|
|
48
|
-
* dialect difference. Stripping is infallible, so it returns a plain value rather than a
|
|
49
|
-
* `Result`.
|
|
50
|
-
*
|
|
51
|
-
* `additionalProperties` and `$schema` are stripped only where they appear as schema
|
|
52
|
-
* *keywords* (siblings of `type`/`properties`/etc.). Inside a `properties` map the keys
|
|
53
|
-
* are user-defined parameter names, not keywords, so they are preserved verbatim while
|
|
54
|
-
* each property's subschema value is still recursively sanitized — a tool parameter
|
|
55
|
-
* legitimately named `additionalProperties` survives.
|
|
56
|
-
*
|
|
57
|
-
* @internal
|
|
58
|
-
*/
|
|
59
36
|
export declare function toGeminiParameterSchema(schema: JsonValue): JsonValue;
|
|
60
37
|
/**
|
|
61
38
|
* Formats tool configs for the Gemini generateContent API.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolFormats.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEpE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAEjB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEvB,MAAM,SAAS,CAAC;AAMjB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,qBAAqB,EACjC,aAAa,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,EAChD,YAAY,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAC/C,aAAa,CAAC,kBAAkB,CAAC,CAcnC;AA4CD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAcjG;AA0CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAc9F;
|
|
1
|
+
{"version":3,"file":"toolFormats.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEpE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAEjB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEvB,MAAM,SAAS,CAAC;AAMjB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,qBAAqB,EACjC,aAAa,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,EAChD,YAAY,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAC/C,aAAa,CAAC,kBAAkB,CAAC,CAcnC;AA4CD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAcjG;AA0CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAc9F;AAgDD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CA2CpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CA6B3F"}
|
|
@@ -192,16 +192,52 @@ function toAnthropicTools(tools) {
|
|
|
192
192
|
*
|
|
193
193
|
* @internal
|
|
194
194
|
*/
|
|
195
|
+
/**
|
|
196
|
+
* The non-`null` member of a draft-07 nullable `type` union, or `undefined` when `type` is
|
|
197
|
+
* not one.
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* Deliberately narrow: only the two-member `[<type>, 'null']` shape `JsonSchema` emits is
|
|
201
|
+
* recognised. A general union has no OpenAPI equivalent, so translating one would be
|
|
202
|
+
* inventing a meaning — it is passed through unchanged and Gemini refuses it, which is the
|
|
203
|
+
* honest outcome.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
function _nullableUnionMember(type) {
|
|
207
|
+
if (!Array.isArray(type) || type.length !== 2 || !type.includes('null')) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const other = type.find((member) => member !== 'null');
|
|
211
|
+
return typeof other === 'string' ? other : undefined;
|
|
212
|
+
}
|
|
195
213
|
function toGeminiParameterSchema(schema) {
|
|
196
214
|
if (Array.isArray(schema)) {
|
|
197
215
|
return schema.map(toGeminiParameterSchema);
|
|
198
216
|
}
|
|
199
217
|
if (schema !== null && typeof schema === 'object') {
|
|
200
218
|
const out = {};
|
|
219
|
+
// Nullability is spelled differently in the two dialects and they are mutually
|
|
220
|
+
// exclusive: draft-07 (and OpenAI strict mode) wants `type: ['string', 'null']`,
|
|
221
|
+
// OpenAPI 3.0 (and Gemini) wants `type: 'string'` + `nullable: true` and rejects the
|
|
222
|
+
// union array. This is the same class of translation as the `additionalProperties`
|
|
223
|
+
// strip above — a dialect difference the consumer should not have to know about.
|
|
224
|
+
const nullableType = _nullableUnionMember(schema.type);
|
|
201
225
|
for (const [key, value] of Object.entries(schema)) {
|
|
202
226
|
if (key === 'additionalProperties' || key === '$schema') {
|
|
203
227
|
continue;
|
|
204
228
|
}
|
|
229
|
+
if (nullableType !== undefined && key === 'type') {
|
|
230
|
+
out.type = nullableType;
|
|
231
|
+
out.nullable = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (nullableType !== undefined && key === 'enum' && Array.isArray(value)) {
|
|
235
|
+
// A nullable enum carries `null` among its values in draft-07. OpenAPI expresses
|
|
236
|
+
// that with `nullable` alone, so the member is dropped rather than sent as a value
|
|
237
|
+
// Gemini would reject.
|
|
238
|
+
out.enum = value.filter((member) => member !== null);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
205
241
|
if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
206
242
|
// `properties` maps user-defined parameter names to subschemas: recurse each
|
|
207
243
|
// subschema value but never treat a parameter name as a strippable keyword.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolFormats.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.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 * Provider-specific tool format translation and tool resolution logic.\n * @packageDocumentation\n */\n\nimport { type JsonObject, type JsonValue } from '@fgv/ts-json-base';\n\nimport {\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiClientToolConfig,\n type IAiProviderDescriptor,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig\n} from './model';\n\n// ============================================================================\n// Tool resolution\n// ============================================================================\n\n/**\n * Resolves the effective tools for a completion call.\n *\n * - If per-call tools are provided, they override settings-level tools entirely.\n * - Otherwise, settings-level enabled tools are used.\n * - Only tools supported by the provider are included.\n * - Returns an empty array if no tools are enabled (= no tools sent).\n *\n * @param descriptor - The provider descriptor (used to filter by supported tools)\n * @param settingsTools - Tool enablement from provider settings (optional)\n * @param perCallTools - Per-call tool override (optional)\n * @returns The resolved list of tool configs to include in the request\n * @public\n */\nexport function resolveEffectiveTools(\n descriptor: IAiProviderDescriptor,\n settingsTools?: ReadonlyArray<IAiToolEnablement>,\n perCallTools?: ReadonlyArray<AiServerToolConfig>\n): ReadonlyArray<AiServerToolConfig> {\n const supported = new Set(descriptor.supportedTools);\n\n if (perCallTools !== undefined) {\n return perCallTools.filter((t) => supported.has(t.type));\n }\n\n if (settingsTools === undefined) {\n return [];\n }\n\n return settingsTools\n .filter((e) => e.enabled && supported.has(e.type))\n .map((e): AiServerToolConfig => e.config ?? { type: e.type });\n}\n\n// ============================================================================\n// OpenAI / xAI Responses API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction webSearchToResponsesApi(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = { type: 'web_search' };\n\n if (config.allowedDomains || config.blockedDomains) {\n const filters: Record<string, unknown> = {};\n if (config.allowedDomains) {\n filters.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n filters.excluded_domains = [...config.blockedDomains];\n }\n tool.filters = filters;\n }\n\n if (config.enableImageUnderstanding) {\n tool.enable_image_understanding = true;\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction clientToolToResponsesApi(config: IAiClientToolConfig): JsonObject {\n return {\n type: 'function',\n name: config.name,\n description: config.description,\n parameters: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the xAI/OpenAI Responses API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toResponsesApiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToResponsesApi(t);\n case 'client_tool':\n return clientToolToResponsesApi(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Anthropic Messages API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the Anthropic Messages API.\n * @internal\n */\nfunction webSearchToAnthropic(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = {\n type: 'web_search_20250305',\n name: 'web_search'\n };\n\n if (config.maxUses !== undefined) {\n tool.max_uses = config.maxUses;\n }\n if (config.allowedDomains) {\n tool.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n tool.blocked_domains = [...config.blockedDomains];\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the Anthropic Messages API.\n * Note: Anthropic client tools have no `type` field (unlike server tools).\n * @internal\n */\nfunction clientToolToAnthropic(config: IAiClientToolConfig): JsonObject {\n return {\n name: config.name,\n description: config.description,\n input_schema: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the Anthropic Messages API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toAnthropicTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToAnthropic(t);\n case 'client_tool':\n return clientToolToAnthropic(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Gemini generateContent API format\n// ============================================================================\n\n/**\n * Sanitizes a draft-07 JSON Schema (as emitted by `JsonSchema.object(...).toJson()`)\n * into the OpenAPI 3.0 Schema Object subset that Gemini's `function_declarations[].parameters`\n * accepts.\n *\n * @remarks\n * Gemini's function-declaration schema is **not** full JSON Schema — it is a subset of\n * the OpenAPI 3.0 Schema Object and **rejects** (rather than ignores) draft-07-only\n * keywords. `JsonSchema` objects are strict-by-default, so `.toJson()` emits\n * `additionalProperties: false` on every object node, which 400s the whole request on\n * Gemini. This helper recursively strips the unsupported keywords so any\n * `JsonSchema`-authored client tool works on Gemini without consumer awareness of the\n * dialect difference. Stripping is infallible, so it returns a plain value rather than a\n * `Result`.\n *\n * `additionalProperties` and `$schema` are stripped only where they appear as schema\n * *keywords* (siblings of `type`/`properties`/etc.). Inside a `properties` map the keys\n * are user-defined parameter names, not keywords, so they are preserved verbatim while\n * each property's subschema value is still recursively sanitized — a tool parameter\n * legitimately named `additionalProperties` survives.\n *\n * @internal\n */\nexport function toGeminiParameterSchema(schema: JsonValue): JsonValue {\n if (Array.isArray(schema)) {\n return schema.map(toGeminiParameterSchema);\n }\n if (schema !== null && typeof schema === 'object') {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(schema)) {\n if (key === 'additionalProperties' || key === '$schema') {\n continue;\n }\n if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n // `properties` maps user-defined parameter names to subschemas: recurse each\n // subschema value but never treat a parameter name as a strippable keyword.\n const properties: JsonObject = {};\n for (const [name, propSchema] of Object.entries(value)) {\n properties[name] = toGeminiParameterSchema(propSchema);\n }\n out[key] = properties;\n } else {\n out[key] = toGeminiParameterSchema(value);\n }\n }\n return out;\n }\n return schema;\n}\n\n/**\n * Formats tool configs for the Gemini generateContent API.\n *\n * @remarks\n * Gemini uses `google_search` for search grounding (no per-tool config).\n * Client-defined tools are accumulated into a single `function_declarations` entry.\n * Each client tool's parameters schema is sanitized to Gemini's OpenAPI-subset\n * dialect via {@link toGeminiParameterSchema} (the raw draft-07 `.toJson()` output\n * carries `additionalProperties`, which Gemini rejects).\n *\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toGeminiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n const result: JsonObject[] = [];\n const functionDeclarations: JsonObject[] = [];\n\n for (const t of tools) {\n switch (t.type) {\n case 'web_search':\n result.push({ google_search: {} } as JsonObject);\n break;\n case 'client_tool':\n functionDeclarations.push({\n name: t.name,\n description: t.description,\n parameters: toGeminiParameterSchema(t.parametersSchema.toJson())\n } as JsonObject);\n break;\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n result.push({ type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject);\n }\n }\n }\n\n if (functionDeclarations.length > 0) {\n result.push({ function_declarations: functionDeclarations } as JsonObject);\n }\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"toolFormats.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/toolFormats.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 * Provider-specific tool format translation and tool resolution logic.\n * @packageDocumentation\n */\n\nimport { type JsonObject, type JsonValue } from '@fgv/ts-json-base';\n\nimport {\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiClientToolConfig,\n type IAiProviderDescriptor,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig\n} from './model';\n\n// ============================================================================\n// Tool resolution\n// ============================================================================\n\n/**\n * Resolves the effective tools for a completion call.\n *\n * - If per-call tools are provided, they override settings-level tools entirely.\n * - Otherwise, settings-level enabled tools are used.\n * - Only tools supported by the provider are included.\n * - Returns an empty array if no tools are enabled (= no tools sent).\n *\n * @param descriptor - The provider descriptor (used to filter by supported tools)\n * @param settingsTools - Tool enablement from provider settings (optional)\n * @param perCallTools - Per-call tool override (optional)\n * @returns The resolved list of tool configs to include in the request\n * @public\n */\nexport function resolveEffectiveTools(\n descriptor: IAiProviderDescriptor,\n settingsTools?: ReadonlyArray<IAiToolEnablement>,\n perCallTools?: ReadonlyArray<AiServerToolConfig>\n): ReadonlyArray<AiServerToolConfig> {\n const supported = new Set(descriptor.supportedTools);\n\n if (perCallTools !== undefined) {\n return perCallTools.filter((t) => supported.has(t.type));\n }\n\n if (settingsTools === undefined) {\n return [];\n }\n\n return settingsTools\n .filter((e) => e.enabled && supported.has(e.type))\n .map((e): AiServerToolConfig => e.config ?? { type: e.type });\n}\n\n// ============================================================================\n// OpenAI / xAI Responses API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction webSearchToResponsesApi(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = { type: 'web_search' };\n\n if (config.allowedDomains || config.blockedDomains) {\n const filters: Record<string, unknown> = {};\n if (config.allowedDomains) {\n filters.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n filters.excluded_domains = [...config.blockedDomains];\n }\n tool.filters = filters;\n }\n\n if (config.enableImageUnderstanding) {\n tool.enable_image_understanding = true;\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the xAI/OpenAI Responses API.\n * @internal\n */\nfunction clientToolToResponsesApi(config: IAiClientToolConfig): JsonObject {\n return {\n type: 'function',\n name: config.name,\n description: config.description,\n parameters: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the xAI/OpenAI Responses API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toResponsesApiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToResponsesApi(t);\n case 'client_tool':\n return clientToolToResponsesApi(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Anthropic Messages API format\n// ============================================================================\n\n/**\n * Formats a web search tool config for the Anthropic Messages API.\n * @internal\n */\nfunction webSearchToAnthropic(config: IAiWebSearchToolConfig): JsonObject {\n const tool: Record<string, unknown> = {\n type: 'web_search_20250305',\n name: 'web_search'\n };\n\n if (config.maxUses !== undefined) {\n tool.max_uses = config.maxUses;\n }\n if (config.allowedDomains) {\n tool.allowed_domains = [...config.allowedDomains];\n }\n if (config.blockedDomains) {\n tool.blocked_domains = [...config.blockedDomains];\n }\n\n return tool as JsonObject;\n}\n\n/**\n * Formats a client tool config for the Anthropic Messages API.\n * Note: Anthropic client tools have no `type` field (unlike server tools).\n * @internal\n */\nfunction clientToolToAnthropic(config: IAiClientToolConfig): JsonObject {\n return {\n name: config.name,\n description: config.description,\n input_schema: config.parametersSchema.toJson()\n } as JsonObject;\n}\n\n/**\n * Formats tool configs for the Anthropic Messages API.\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toAnthropicTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n return tools.map((t) => {\n switch (t.type) {\n case 'web_search':\n return webSearchToAnthropic(t);\n case 'client_tool':\n return clientToolToAnthropic(t);\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n return { type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject;\n }\n }\n });\n}\n\n// ============================================================================\n// Gemini generateContent API format\n// ============================================================================\n\n/**\n * Sanitizes a draft-07 JSON Schema (as emitted by `JsonSchema.object(...).toJson()`)\n * into the OpenAPI 3.0 Schema Object subset that Gemini's `function_declarations[].parameters`\n * accepts.\n *\n * @remarks\n * Gemini's function-declaration schema is **not** full JSON Schema — it is a subset of\n * the OpenAPI 3.0 Schema Object and **rejects** (rather than ignores) draft-07-only\n * keywords. `JsonSchema` objects are strict-by-default, so `.toJson()` emits\n * `additionalProperties: false` on every object node, which 400s the whole request on\n * Gemini. This helper recursively strips the unsupported keywords so any\n * `JsonSchema`-authored client tool works on Gemini without consumer awareness of the\n * dialect difference. Stripping is infallible, so it returns a plain value rather than a\n * `Result`.\n *\n * `additionalProperties` and `$schema` are stripped only where they appear as schema\n * *keywords* (siblings of `type`/`properties`/etc.). Inside a `properties` map the keys\n * are user-defined parameter names, not keywords, so they are preserved verbatim while\n * each property's subschema value is still recursively sanitized — a tool parameter\n * legitimately named `additionalProperties` survives.\n *\n * @internal\n */\n/**\n * The non-`null` member of a draft-07 nullable `type` union, or `undefined` when `type` is\n * not one.\n *\n * @remarks\n * Deliberately narrow: only the two-member `[<type>, 'null']` shape `JsonSchema` emits is\n * recognised. A general union has no OpenAPI equivalent, so translating one would be\n * inventing a meaning — it is passed through unchanged and Gemini refuses it, which is the\n * honest outcome.\n * @internal\n */\nfunction _nullableUnionMember(type: JsonValue | undefined): string | undefined {\n if (!Array.isArray(type) || type.length !== 2 || !type.includes('null')) {\n return undefined;\n }\n const other: JsonValue | undefined = type.find((member) => member !== 'null');\n return typeof other === 'string' ? other : undefined;\n}\n\nexport function toGeminiParameterSchema(schema: JsonValue): JsonValue {\n if (Array.isArray(schema)) {\n return schema.map(toGeminiParameterSchema);\n }\n if (schema !== null && typeof schema === 'object') {\n const out: JsonObject = {};\n // Nullability is spelled differently in the two dialects and they are mutually\n // exclusive: draft-07 (and OpenAI strict mode) wants `type: ['string', 'null']`,\n // OpenAPI 3.0 (and Gemini) wants `type: 'string'` + `nullable: true` and rejects the\n // union array. This is the same class of translation as the `additionalProperties`\n // strip above — a dialect difference the consumer should not have to know about.\n const nullableType: string | undefined = _nullableUnionMember(schema.type);\n for (const [key, value] of Object.entries(schema)) {\n if (key === 'additionalProperties' || key === '$schema') {\n continue;\n }\n if (nullableType !== undefined && key === 'type') {\n out.type = nullableType;\n out.nullable = true;\n continue;\n }\n if (nullableType !== undefined && key === 'enum' && Array.isArray(value)) {\n // A nullable enum carries `null` among its values in draft-07. OpenAPI expresses\n // that with `nullable` alone, so the member is dropped rather than sent as a value\n // Gemini would reject.\n out.enum = value.filter((member) => member !== null);\n continue;\n }\n if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n // `properties` maps user-defined parameter names to subschemas: recurse each\n // subschema value but never treat a parameter name as a strippable keyword.\n const properties: JsonObject = {};\n for (const [name, propSchema] of Object.entries(value)) {\n properties[name] = toGeminiParameterSchema(propSchema);\n }\n out[key] = properties;\n } else {\n out[key] = toGeminiParameterSchema(value);\n }\n }\n return out;\n }\n return schema;\n}\n\n/**\n * Formats tool configs for the Gemini generateContent API.\n *\n * @remarks\n * Gemini uses `google_search` for search grounding (no per-tool config).\n * Client-defined tools are accumulated into a single `function_declarations` entry.\n * Each client tool's parameters schema is sanitized to Gemini's OpenAPI-subset\n * dialect via {@link toGeminiParameterSchema} (the raw draft-07 `.toJson()` output\n * carries `additionalProperties`, which Gemini rejects).\n *\n * @param tools - The resolved tool configs (server-side and/or client-defined)\n * @returns Provider-native tool objects for the `tools` request field\n * @public\n */\nexport function toGeminiTools(tools: ReadonlyArray<AiToolConfig>): ReadonlyArray<JsonObject> {\n const result: JsonObject[] = [];\n const functionDeclarations: JsonObject[] = [];\n\n for (const t of tools) {\n switch (t.type) {\n case 'web_search':\n result.push({ google_search: {} } as JsonObject);\n break;\n case 'client_tool':\n functionDeclarations.push({\n name: t.name,\n description: t.description,\n parameters: toGeminiParameterSchema(t.parametersSchema.toJson())\n } as JsonObject);\n break;\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = t;\n result.push({ type: `unknown:${JSON.stringify(_exhaustive)}` } as JsonObject);\n }\n }\n }\n\n if (functionDeclarations.length > 0) {\n result.push({ function_declarations: functionDeclarations } as JsonObject);\n }\n\n return result;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgv/ts-extras",
|
|
3
|
-
"version": "5.1.0-
|
|
3
|
+
"version": "5.1.0-55",
|
|
4
4
|
"description": "Assorted Typescript Utilities",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "dist/ts-extras.d.ts",
|
|
@@ -100,10 +100,10 @@
|
|
|
100
100
|
"@types/js-yaml": "~4.0.9",
|
|
101
101
|
"typedoc": "~0.28.16",
|
|
102
102
|
"typedoc-plugin-markdown": "~4.9.0",
|
|
103
|
-
"@fgv/heft-dual-rig": "5.1.0-
|
|
104
|
-
"@fgv/
|
|
105
|
-
"@fgv/
|
|
106
|
-
"@fgv/ts-utils": "5.1.0-
|
|
103
|
+
"@fgv/heft-dual-rig": "5.1.0-55",
|
|
104
|
+
"@fgv/typedoc-compact-theme": "5.1.0-55",
|
|
105
|
+
"@fgv/ts-utils-jest": "5.1.0-55",
|
|
106
|
+
"@fgv/ts-utils": "5.1.0-55"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"@types/luxon": "^3.7.1",
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
"papaparse": "^5.4.1",
|
|
113
113
|
"fflate": "~0.8.2",
|
|
114
114
|
"js-yaml": "~4.1.1",
|
|
115
|
-
"@fgv/ts-json-base": "5.1.0-
|
|
115
|
+
"@fgv/ts-json-base": "5.1.0-55"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
|
118
|
-
"@fgv/ts-utils": "5.1.0-
|
|
118
|
+
"@fgv/ts-utils": "5.1.0-55"
|
|
119
119
|
},
|
|
120
120
|
"repository": {
|
|
121
121
|
"type": "git",
|