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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user