diff --git a/src/adapters/responses-tool-schema.ts b/src/adapters/responses-tool-schema.ts index 7d10b9beec..6b28cca6ea 100644 --- a/src/adapters/responses-tool-schema.ts +++ b/src/adapters/responses-tool-schema.ts @@ -107,66 +107,96 @@ function usesUnicodePropertyEscape(pattern: string): boolean { * preserved: loosening a nested constraint can instead reject an input in those contexts. * Unsupported patterns there remain the destination's validation responsibility. * - * Returns `node` itself when nothing was dropped. Uses an explicit stack for caller-controlled - * nesting depth; the separate Responses-only encrypted-marker normalization is unchanged. + * Returns `node` itself when nothing was dropped. Traversal keeps only the active path and clones + * only ancestors of a removed constraint, so a broad no-op schema does not create an output tree + * or one pending closure per sibling. The explicit stack still handles caller-controlled nesting + * depth; the separate Responses-only encrypted-marker normalization is unchanged. */ export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown { - type Assign = (value: unknown) => void; - interface Frame { node: unknown; inNameBag: boolean; assign: Assign } - - let result: unknown; - let dropped = 0; - const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }]; + interface Frame { + node: unknown[] | Record; + inNameBag: boolean; + parent?: Frame; + parentKey?: string | number; + output?: unknown[] | Record; + index?: number; + entries?: IterableIterator<[string, unknown]>; + } - while (stack.length > 0) { - const frame = stack.pop()!; - const current = frame.node; + function * ownEntries(value: Record): IterableIterator<[string, unknown]> { + // Unlike Object.entries(), this does not materialize every key/value pair before traversal. + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) yield [key, value[key]]; + } + } - if (Array.isArray(current)) { - const out: unknown[] = new Array(current.length); - frame.assign(out); - // Array items are schemas in their own right, never a name bag. - for (let i = current.length - 1; i >= 0; i--) { - stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } }); - } - continue; + function cloneContainer(frame: Frame): unknown[] | Record { + if (frame.output) return frame.output; + if (Array.isArray(frame.node)) { + frame.output = frame.node.slice(); + return frame.output; } - if (!current || typeof current !== "object") { - frame.assign(current); - continue; + const output: Record = Object.create(null) as Record; + for (const key in frame.node) { + if (Object.prototype.hasOwnProperty.call(frame.node, key)) output[key] = frame.node[key]; } + frame.output = output; + return output; + } - // A schema name may be `__proto__`; a null-prototype record keeps it as data. - const out: Record = Object.create(null) as Record; - frame.assign(out); + function finish(frame: Frame): void { + if (!frame.output || !frame.parent) return; + const parent = cloneContainer(frame.parent); + if (Array.isArray(parent)) parent[frame.parentKey as number] = frame.output; + else parent[frame.parentKey as string] = frame.output; + } - for (const [key, value] of Object.entries(current as Record)) { - if (frame.inNameBag) { - // Inside a name bag every key is a caller-chosen name, so `pattern` here is a property - // name; its value is still a schema and is walked as one. - stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } }); - continue; - } - if (PRESERVED_PATTERN_SUBTREES.has(key)) { - out[key] = value; - continue; - } - if (key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) { - dropped++; + if (!node || typeof node !== "object") return node; + const root: Frame = { node: node as unknown[] | Record, inNameBag }; + const stack: Frame[] = [root]; + + while (stack.length > 0) { + const frame = stack[stack.length - 1]!; + + if (Array.isArray(frame.node)) { + const index = frame.index ?? 0; + if (index >= frame.node.length) { + stack.pop(); + finish(frame); continue; } - if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) { - // Literal payloads are values, not schemas: a `pattern` key inside them is data. - out[key] = value; - continue; + frame.index = index + 1; + const child = frame.node[index]; + if (child && typeof child === "object") { + stack.push({ node: child as unknown[] | Record, inNameBag: false, parent: frame, parentKey: index }); } + continue; + } + + frame.entries ??= ownEntries(frame.node); + const next = frame.entries.next(); + if (next.done) { + stack.pop(); + finish(frame); + continue; + } + const [key, value] = next.value; + if (!frame.inNameBag && key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) { + delete (cloneContainer(frame) as Record)[key]; + continue; + } + if (!frame.inNameBag && (PRESERVED_PATTERN_SUBTREES.has(key) || SCHEMA_LITERAL_VALUE_KEYS.has(key))) { + continue; + } + if (value && typeof value === "object") { stack.push({ - node: value, - inNameBag: SCHEMA_NAME_BAG_KEYS.has(key), - assign: v => { out[key] = v; }, + node: value as unknown[] | Record, + inNameBag: !frame.inNameBag && SCHEMA_NAME_BAG_KEYS.has(key), + parent: frame, + parentKey: key, }); } } - return dropped === 0 ? node : result; + return root.output ?? node; } diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 47441e57cb..873de72467 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -202,3 +202,5 @@ Shared response-log retention and native SSE inspection pacing follow the [bound Native steering retains fixed phase deadlines and reconciled replay output; see the [steering stability contract](../transports/streaming-health.md#steering-deadlines-and-replay-completeness). Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the [shared control contract](../transports/streaming-health.md#steering-settings-public-api-and-diagnostic-probe); this owner does not change routing or execute diagnostic tools. + +Unicode pattern normalization uses [copy-on-write traversal](../transports/byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 4117495323..151df81275 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -342,3 +342,5 @@ Shared response-log retention and native SSE inspection pacing follow the [bound Native steering retains fixed phase deadlines and reconciled replay output; see the [steering stability contract](../transports/streaming-health.md#steering-deadlines-and-replay-completeness). Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the [shared control contract](../transports/streaming-health.md#steering-settings-public-api-and-diagnostic-probe); this owner does not change routing or execute diagnostic tools. + +Unicode pattern normalization uses [copy-on-write traversal](../transports/byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e164143c2b..2c1507a458 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -393,3 +393,5 @@ refusal of original images is unchanged. Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. Upstream API-key usage follows the [physical-attempt account attribution contract](../gui-and-management-api.md#upstream-key-account-attribution), independently of subscription quota observations. + +Unicode pattern normalization uses [copy-on-write traversal](../transports/byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index e05b51d76e..8be236cd91 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -217,3 +217,5 @@ Translated audio/file admission follows the [final-adapter input contract](../ad Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. Upstream API-key usage follows the [physical-attempt account attribution contract](../gui-and-management-api.md#upstream-key-account-attribution), independently of subscription quota observations. + +Unicode pattern normalization uses [copy-on-write traversal](../transports/byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. diff --git a/structure/runtime.md b/structure/runtime.md index 4b57292a16..ed2bffbf75 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -552,3 +552,5 @@ defines identity, unknown records, and aggregation boundaries. Native steering retains fixed phase deadlines and reconciled replay output; see the [steering stability contract](transports/streaming-health.md#steering-deadlines-and-replay-completeness). Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the [shared control contract](transports/streaming-health.md#steering-settings-public-api-and-diagnostic-probe); this owner does not change routing or execute diagnostic tools. + +Unicode pattern normalization uses [copy-on-write traversal](transports/byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 2926e8d253..45215a29cf 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -134,3 +134,16 @@ The same focused tests cover these lifecycle paths and Unicode code-unit limit b Native steering retains fixed phase deadlines and reconciled replay output; see the [steering stability contract](../transports/streaming-health.md#steering-deadlines-and-replay-completeness). Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the [shared control contract](streaming-health.md#steering-settings-public-api-and-diagnostic-probe); this owner does not change routing or execute diagnostic tools. + +## Unicode pattern normalization + +`src/adapters/responses-tool-schema.ts` strips unsupported Unicode property patterns with an +iterative traversal and copies containers only when a descendant changes. Unchanged siblings +retain identity; a no-op returns the original input. Traversal frames follow the active path +instead of queueing an assignment closure and eagerly cloned container for each sibling. +Name bags, literal values and preserved constraint subtrees retain their existing semantics; +the separate encrypted-marker normalizer is unchanged. Inputs are not mutated. +This reduces avoidable allocations; it is not a hard heap cap or a guarantee of lower CPU cost. +Schema size still determines traversal work and the cost of copying a changed broad container. +`tests/adapters/openai/openai-chat-hardening.test.ts` covers wide, deep and mixed-array schemas; +`tests/responses/openai-responses-passthrough.test.ts` covers the existing wire contract. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index fe5f93794c..8613a25579 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -199,6 +199,8 @@ Native steering retains fixed phase deadlines and reconciled replay output; see Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the [shared control contract](streaming-health.md#steering-settings-public-api-and-diagnostic-probe); this owner does not change routing or execute diagnostic tools. +Unicode pattern normalization uses [copy-on-write traversal](byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. + ## Model-family-aware OAuth headroom `src/oauth/account-quota-rank.ts` ranks Antigravity custom windows for the requested diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 403d434a2e..1647efaa11 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -807,7 +807,7 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. Unicode pattern normalization uses [copy-on-write traversal](byte-accounting.md#unicode-pattern-normalization) while preserving the existing schema and wire semantics. ## Core module ownership diff --git a/tests/adapters/openai/openai-chat-hardening.test.ts b/tests/adapters/openai/openai-chat-hardening.test.ts index 3a8fe98f03..492dca51c0 100644 --- a/tests/adapters/openai/openai-chat-hardening.test.ts +++ b/tests/adapters/openai/openai-chat-hardening.test.ts @@ -325,6 +325,36 @@ describe("unicode property-escape pattern stripping", () => { expect(stripped.properties.plain.pattern).toBe("^[a-z0-9_-]{1,64}$"); }); + test("clones only affected paths across a broad schema", () => { + const properties: Record> = {}; + for (let i = 0; i < 25_000; i++) properties[`field_${i}`] = { type: "string" }; + properties.affected = { type: "string", pattern: artifactFieldPattern }; + const before = { type: "object", properties }; + const stripped = stripUnicodePropertyPatterns(before) as typeof before; + + expect(stripped).not.toBe(before); + expect(stripped.properties).not.toBe(properties); + expect(stripped.properties.affected.pattern).toBeUndefined(); + expect(properties.affected.pattern).toBe(artifactFieldPattern); + expect(stripped.properties.field_0).toBe(properties.field_0); + expect(stripped.properties.field_24999).toBe(properties.field_24999); + }); + + test("copies changed array paths while preserving literal and untouched siblings", () => { + const literal = { pattern: artifactFieldPattern }; + const untouched = { type: "string", pattern: "^[a-z]+$" }; + const changed = { type: "string", pattern: artifactFieldPattern, const: literal }; + const before = { allOf: [changed, untouched, { properties: { pattern: changed } }] }; + const stripped = stripUnicodePropertyPatterns(before) as typeof before; + + expect(stripped.allOf).not.toBe(before.allOf); + expect(stripped.allOf[0]).toEqual({ type: "string", const: literal }); + expect(stripped.allOf[0]!.const).toBe(literal); + expect(stripped.allOf[1]).toBe(untouched); + expect(stripped.allOf[2]!.properties!.pattern.pattern).toBeUndefined(); + expect(changed.pattern).toBe(artifactFieldPattern); + }); + test("an escaped backslash before `p{` is a literal, not a property escape", () => { // `\\p{2}` is a literal backslash followed by a quantified `p`; Python compiles it, so a // substring scan for `\p{` would throw away a working pattern.