fix(ai,coding-agent): graceful strict-schema degradation for patternProperties

- Add hasUnrepresentableStrictObjectMap() pre-flight in tryEnforceStrictSchema;
  schemas using patternProperties or schema-valued additionalProperties now
  degrade to non-strict mode instead of throwing during enforcement
- Add test coverage for patternProperties graceful degradation
- Extend ANTHROPIC_OAUTH_BETA in openai-compat route with
  interleaved-thinking-2025-05-14, context-management-2025-06-27, and
  prompt-caching-scope-2026-01-05 beta flags
- Add gemini, codex, and synthetic to providers.webSearch setting enum
This commit is contained in:
can1357
2026-02-28 20:47:41 +01:00
parent e1a7b6360e
commit 62dcb9f1ca
6 changed files with 103 additions and 2 deletions
+8
View File
@@ -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
@@ -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;
@@ -29,6 +29,61 @@ export function StringEnum<const T extends readonly string[]>(
export const NO_STRICT = Bun.env.PI_NO_STRICT === "1";
const strictSchemaCache = new WeakMap<Record<string, unknown>, { schema: Record<string, unknown>; strict: boolean }>();
function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, seen?: WeakSet<object>): 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<string, unknown>,
seen?: WeakSet<object>,
@@ -283,6 +338,9 @@ export function tryEnforceStrictSchema(schema: Record<string, unknown>): {
}
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);
+18
View File
@@ -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);
});
});
+4
View File
@@ -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
@@ -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 },
},