diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 976598359..204e77408 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- `hasUnrepresentableStrictObjectMap()` pre-flight check in `tryEnforceStrictSchema`: schemas with `patternProperties` or schema-valued `additionalProperties` now degrade gracefully to non-strict mode instead of throwing during enforcement + +### Changed + +- Extended `ANTHROPIC_OAUTH_BETA` constant in the OpenAI-compat Anthropic route with `interleaved-thinking-2025-05-14`, `context-management-2025-06-27`, and `prompt-caching-scope-2026-01-05` beta flags + ## [13.3.14] - 2026-02-28 ### Added diff --git a/packages/ai/src/provider-models/openai-compat.ts b/packages/ai/src/provider-models/openai-compat.ts index f9587c454..3e05e8be7 100644 --- a/packages/ai/src/provider-models/openai-compat.ts +++ b/packages/ai/src/provider-models/openai-compat.ts @@ -10,7 +10,8 @@ import { const MODELS_DEV_URL = "https://models.dev/api.json"; const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1"; -const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20"; +const ANTHROPIC_OAUTH_BETA = + "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"; export interface ModelsDevModel { id?: string; diff --git a/packages/ai/src/utils/schema/strict-mode.ts b/packages/ai/src/utils/schema/strict-mode.ts index 13c5fedaa..19c0a5e8f 100644 --- a/packages/ai/src/utils/schema/strict-mode.ts +++ b/packages/ai/src/utils/schema/strict-mode.ts @@ -29,6 +29,61 @@ export function StringEnum( export const NO_STRICT = Bun.env.PI_NO_STRICT === "1"; const strictSchemaCache = new WeakMap, { schema: Record; strict: boolean }>(); +function hasUnrepresentableStrictObjectMap(schema: Record, seen?: WeakSet): boolean { + if (!seen) seen = new WeakSet(); + if (seen.has(schema)) return false; + seen.add(schema); + + const hasPatternProperties = + isJsonObject(schema.patternProperties) && Object.keys(schema.patternProperties).length > 0; + const additionalPropertiesValue = schema.additionalProperties; + const hasSchemaAdditionalProperties = additionalPropertiesValue === true || isJsonObject(additionalPropertiesValue); + if (hasPatternProperties || hasSchemaAdditionalProperties) { + return true; + } + + if (isJsonObject(schema.properties)) { + for (const propertySchema of Object.values(schema.properties)) { + if (isJsonObject(propertySchema) && hasUnrepresentableStrictObjectMap(propertySchema, seen)) { + return true; + } + } + } + + if (isJsonObject(schema.items)) { + if (hasUnrepresentableStrictObjectMap(schema.items, seen)) { + return true; + } + } else if (Array.isArray(schema.items)) { + for (const itemSchema of schema.items) { + if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, seen)) { + return true; + } + } + } + + for (const key of COMBINATOR_KEYS) { + const variants = schema[key]; + if (!Array.isArray(variants)) continue; + for (const variant of variants) { + if (isJsonObject(variant) && hasUnrepresentableStrictObjectMap(variant, seen)) { + return true; + } + } + } + + for (const defsKey of ["$defs", "definitions"] as const) { + const defs = schema[defsKey]; + if (!isJsonObject(defs)) continue; + for (const defSchema of Object.values(defs)) { + if (isJsonObject(defSchema) && hasUnrepresentableStrictObjectMap(defSchema, seen)) { + return true; + } + } + } + + return false; +} export function sanitizeSchemaForStrictMode( schema: Record, seen?: WeakSet, @@ -283,6 +338,9 @@ export function tryEnforceStrictSchema(schema: Record): { } try { + if (hasUnrepresentableStrictObjectMap(schema)) { + throw new Error("Schema uses dynamic object keys that are not representable in strict mode"); + } const sanitized = sanitizeSchemaForStrictMode(schema); const result = { schema: enforceStrictSchema(sanitized), strict: true }; strictSchemaCache.set(schema, result); diff --git a/packages/ai/test/schema-helpers.test.ts b/packages/ai/test/schema-helpers.test.ts index bd0d2ea7d..daf292d4a 100644 --- a/packages/ai/test/schema-helpers.test.ts +++ b/packages/ai/test/schema-helpers.test.ts @@ -242,4 +242,22 @@ describe("adaptSchemaForStrict", () => { expect(result.strict).toBe(false); expect(result.schema).toBe(schema); }); + + it("degrades gracefully for schemas with patternProperties maps", () => { + const schema = { + type: "object", + properties: { + rewrites: { + type: "object", + patternProperties: { + "^(.*)$": { type: "string" }, + }, + }, + }, + required: ["rewrites"], + }; + const result = adaptSchemaForStrict(schema, true); + expect(result.strict).toBe(false); + expect(result.schema).toBe(schema); + }); }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7ac5956dc..6c2c8a29e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `gemini`, `codex`, and `synthetic` as supported values for the `providers.webSearch` setting + ## [13.3.14] - 2026-02-28 ### Added diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 93bb544cd..9e38f4b0d 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -737,7 +737,19 @@ export const SETTINGS_SCHEMA = { // ───────────────────────────────────────────────────────────────────────── "providers.webSearch": { type: "enum", - values: ["auto", "exa", "brave", "jina", "kimi", "zai", "perplexity", "anthropic"] as const, + values: [ + "auto", + "exa", + "brave", + "jina", + "kimi", + "zai", + "perplexity", + "anthropic", + "gemini", + "codex", + "synthetic", + ] as const, default: "auto", ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true }, },