diff --git a/scripts/test-layout/layout-responses.json b/scripts/test-layout/layout-responses.json index 306719dbad2..0966bc5bc37 100644 --- a/scripts/test-layout/layout-responses.json +++ b/scripts/test-layout/layout-responses.json @@ -1,4 +1,5 @@ { + "responses-native-output-identity.test.ts": "responses", "graceful-max-output-tokens.test.ts": "responses", "graceful-max-output-tokens-exclusions.test.ts": "responses", "advisor-web-search.test.ts": "advisor", diff --git a/src/server/responses/native-response-json.ts b/src/server/responses/native-response-json.ts index 1692c6b25d1..b94de15a1c5 100644 --- a/src/server/responses/native-response-json.ts +++ b/src/server/responses/native-response-json.ts @@ -5,10 +5,19 @@ export function nativeResponseRecord(value: unknown): value is Record Array.isArray(item) ? `[${item.map(canonical).join(",")}]` + : nativeResponseRecord(item) ? `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(",")}}` + : JSON.stringify(item) ?? "null"; +const fingerprint = (json: string): string => createHash("sha256").update(json).digest("hex"); + /** Compare JSON content by value: object-key order is irrelevant, array order is not. */ export function nativeResponseFingerprint(value: unknown): string { - const canonical = (item: unknown): string => Array.isArray(item) ? `[${item.map(canonical).join(",")}]` - : nativeResponseRecord(item) ? `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(",")}}` - : JSON.stringify(item) ?? "null"; - return createHash("sha256").update(canonical(value)).digest("hex"); + return fingerprint(canonical(value)); +} + +/** Keep transient body keys within the existing 64-byte digest payload bound. */ +export function nativeResponseOutputIdentity(item: Record): string { + if (typeof item.id === "string") return `id:${item.id}`; + const json = canonical(item); + return Buffer.byteLength(json, "utf8") <= 64 ? `json:${json}` : `body:${fingerprint(json)}`; } diff --git a/src/server/responses/native-response-output.ts b/src/server/responses/native-response-output.ts index 04cce41e54e..29ef53a50fe 100644 --- a/src/server/responses/native-response-output.ts +++ b/src/server/responses/native-response-output.ts @@ -1,4 +1,4 @@ -import { nativeResponseFingerprint as injectionFingerprint, nativeResponseRecord as record } from "./native-response-json"; +import { nativeResponseFingerprint as injectionFingerprint, nativeResponseOutputIdentity as identity, nativeResponseRecord as record } from "./native-response-json"; type Frame = Record; /** @@ -10,7 +10,6 @@ export function nativeResponseOutput(done: ReadonlyMap, terminal: const observed = [...done.entries()].sort((a, b) => a[0] - b[0]).map(([, item]) => item); if (terminal == null || (Array.isArray(terminal) && !terminal.length)) return observed; if (!Array.isArray(terminal) || terminal.some(item => !record(item))) throw new Error("Invalid native response output."); - const identity = (item: Frame) => typeof item.id === "string" ? `id:${item.id}` : `body:${injectionFingerprint(item)}`; const positions = new Map(); observed.forEach((item, index) => { const key = identity(item); diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index d0ea8de7b71..22fb88cb6aa 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -32,6 +32,16 @@ Anthropic managed sends recheck [family admission](../providers/anthropic-accoun ## Transport inventory +### Native output identity + +`src/server/responses/native-response-json.ts` compares ID-less output records by canonical JSON. +Transient keys use `json:` plus that JSON when it fits 64 UTF-8 bytes; larger records retain +`body:` plus SHA-256. String IDs retain the distinct `id:` namespace. Body keys remain at most +69 bytes, without skipping duplicate, relative-order or same-ID content checks in +`native-response-output.ts`. Exported fingerprints retain their SHA-256 format. The unchanged +750,000-item replay cases and `tests/responses/responses-native-output-identity.test.ts` cover +key-order equivalence, both key families, sparse output, contradictions and byte bounds. + The sections above cover the transports with load-bearing invariants. The rest of the transport surface is listed here so a maintainer can find the owner without grepping: diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index b9ec2c7a0b8..419316447b4 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -550,9 +550,9 @@ charged against the unchanged 32 MiB serialized history budget. The existing body-persistence eligibility and accepted-only steering commit rules still apply. `tests/responses/ws-steering-stability.test.ts` binds these deadline and replay contracts to deterministic clocks and a synthetic real-handler continuation test. -`src/server/responses/native-response-json.ts` owns content comparison without -importing either control owner, keeping the replay dependency graph acyclic. -Injection retains its existing helper export names and comparison semantics. +`src/server/responses/native-response-json.ts` owns content comparison and +[bounded output identity](inventory.md#native-output-identity) without importing either control owner. +Injection retains its existing helper export names and SHA-256 comparison semantics. ## Steering settings, public API and diagnostic probe diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 7939d5f9243..bcd5f0cc695 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1,5 +1,6 @@ { "lmstudio-system-policy.test.ts": "config", "openai-chat-single-system.test.ts": "adapters/openai", "update-reclaim-authorizer.test.ts": "update", "update-capability-policy.test.ts": "update", "update-service-home.test.ts": "update", "update-service-home-policy.test.ts": "update", "update-home-recovery.test.ts": "update", "update-recovery-authority.test.ts": "update", "update-recovery-boundaries.test.ts": "update", "update-home-launcher.test.ts": "update", "update-home-worker.test.ts": "update", "update-mutation-boundaries.test.ts": "update", "update-mutation-guard.test.ts": "update", "update-guarded-ops.test.ts": "update", "update-effect-authority.test.ts": "update", "update-windows-wrapper-authority.test.ts": "update", "update-mutation-scanner.test.ts": "update", "refreshed-upstream-deadlines.test.ts": "responses", "local-upstream-deadlines.test.ts": "responses", "service-desktop-recovery.test.ts": "service", "provider-input-modalities.test.ts": "providers", "turn-membership.test.ts": "responses", "turn-membership-ingress.test.ts": "responses", "workbuddy-quota.test.ts": "providers", "workbuddy-login.test.ts": "oauth", "workbuddy-adapter.test.ts": "adapters", "workbuddy-provider.test.ts": "providers", "link-tunnel-history.test.ts": "clients", "link-readiness.test.ts": "clients", "link-diagnostics.test.ts": "clients", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "responses-phase-inference.test.ts": "responses", "codex-role-configured-credentials.test.ts": "server", + "responses-native-output-identity.test.ts": "responses", "advisor-caller-policy.test.ts": "advisor", "advisor-caller-policy-sidecar-authority.test.ts": "advisor", "advisor-caller-policy-vision.test.ts": "advisor", diff --git a/tests/responses/responses-native-output-identity.test.ts b/tests/responses/responses-native-output-identity.test.ts new file mode 100644 index 00000000000..5978668337e --- /dev/null +++ b/tests/responses/responses-native-output-identity.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test"; +import { nativeResponseOutput } from "../../src/server/responses/native-response-output"; +import { nativeResponseFingerprint, nativeResponseOutputIdentity } from "../../src/server/responses/native-response-json"; + +test("native output identity preserves key-order equivalence and duplicate refusal", () => { + for (const text of ["short", "한".repeat(40), "x".repeat(1024)]) { + const a = { role: "assistant", content: text }; + const b = { content: text, role: "assistant" }; + expect(nativeResponseOutputIdentity(a)).toBe(nativeResponseOutputIdentity(b)); + expect(nativeResponseOutput(new Map([[0, a]]), [b])).toEqual([b]); + expect(() => nativeResponseOutput(new Map([[0, a], [1, b]]), [a])).toThrow("Duplicate native completed output identity"); + expect(() => nativeResponseOutput(new Map(), [a, b])).toThrow("Duplicate native terminal output identity"); + } +}); + +test("native output identity keeps sparse mixed items ordered and rejects contradictions", () => { + const id = { id: "json:{}", role: "assistant", content: "first" }; + const small = { role: "assistant", content: "second" }; + const large = { role: "assistant", content: "third".repeat(100) }; + const done = new Map([[0, id], [2, small], [9, large]]); + expect(nativeResponseOutput(done, [id, large])).toEqual([id, small, large]); + expect(() => nativeResponseOutput(done, [large, id])).toThrow("contradicts"); + expect(() => nativeResponseOutput(done, [{ ...id, content: "changed" }])).toThrow("contradicts"); + expect(nativeResponseOutput(new Map(), [{ id: "json:{}" }, {}])).toEqual([{ id: "json:{}" }, {}]); +}); + +test("native body identity stays within the former digest key byte bound", () => { + for (const content of ["", "x".repeat(55), "x".repeat(56), "한".repeat(20), "한".repeat(1000)]) { + expect(Buffer.byteLength(nativeResponseOutputIdentity({ content }), "utf8")).toBeLessThanOrEqual(69); + } + expect(nativeResponseOutputIdentity({ content: [1, 2] })).not.toBe(nativeResponseOutputIdentity({ content: [2, 1] })); +}); + +test("the exported native fingerprint retains its SHA-256 contract", () => { + expect(nativeResponseFingerprint({ b: 2, a: 1 })).toBe("43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"); + expect(nativeResponseFingerprint({ a: [1, 2] })).not.toBe(nativeResponseFingerprint({ a: [2, 1] })); + expect(nativeResponseFingerprint({ b: null, a: { y: "한", x: true } })).toBe(nativeResponseFingerprint({ a: { x: true, y: "한" }, b: null })); +}); + +test.each([ + { label: "ASCII 64", content: "x".repeat(50), bytes: 64, prefix: "json:" }, + { label: "ASCII 65", content: "x".repeat(51), bytes: 65, prefix: "body:" }, + { label: "UTF-8 64", content: "é".repeat(25), bytes: 64, prefix: "json:" }, + { label: "UTF-8 65", content: "é".repeat(25) + "x", bytes: 65, prefix: "body:" }, +])("native identity uses the canonical byte boundary: $label", ({ content, bytes, prefix }) => { + const item = { content }; + const json = JSON.stringify(item); + expect(Buffer.byteLength(json, "utf8")).toBe(bytes); + const identity = nativeResponseOutputIdentity(item); + expect(identity.startsWith(prefix)).toBe(true); + if (prefix === "json:") expect(identity).toBe(`json:${json}`); + else expect(identity).toMatch(/^body:[a-f0-9]{64}$/); + expect(Buffer.byteLength(identity, "utf8")).toBeLessThanOrEqual(69); +});