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

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.
@@ -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"]}
@@ -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;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAyBpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CA6B3F"}
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-53",
3
+ "version": "5.1.0-54",
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-53",
104
- "@fgv/ts-utils-jest": "5.1.0-53",
105
- "@fgv/typedoc-compact-theme": "5.1.0-53",
106
- "@fgv/ts-utils": "5.1.0-53"
103
+ "@fgv/typedoc-compact-theme": "5.1.0-54",
104
+ "@fgv/heft-dual-rig": "5.1.0-54",
105
+ "@fgv/ts-utils-jest": "5.1.0-54",
106
+ "@fgv/ts-utils": "5.1.0-54"
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-53"
115
+ "@fgv/ts-json-base": "5.1.0-54"
116
116
  },
117
117
  "peerDependencies": {
118
- "@fgv/ts-utils": "5.1.0-53"
118
+ "@fgv/ts-utils": "5.1.0-54"
119
119
  },
120
120
  "repository": {
121
121
  "type": "git",