diff --git a/docs/openapi.json b/docs/openapi.json index 60d49d1db97..2b8b1692007 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -113153,7 +113153,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -114120,7 +114121,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -114795,7 +114797,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -115459,7 +115462,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -115994,7 +115998,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -116540,7 +116545,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -119037,7 +119043,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -119583,7 +119590,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -120129,7 +120137,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -120675,7 +120684,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -121221,7 +121231,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -121767,7 +121778,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -122313,7 +122325,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -122859,7 +122872,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -123394,7 +123408,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -123929,7 +123944,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -124475,7 +124491,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -125021,7 +125038,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -127738,7 +127756,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -130460,7 +130479,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -131006,7 +131026,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -131786,7 +131807,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -132753,7 +132775,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -133495,13 +133518,17 @@ }, "noToolResults": { "type": "number" + }, + "addonUnavailable": { + "type": "number" } }, "required": [ "applied", "notEnabled", "notEffective", - "noToolResults" + "noToolResults", + "addonUnavailable" ], "additionalProperties": false }, @@ -134804,7 +134831,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -135771,7 +135799,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -136446,7 +136475,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -137110,7 +137140,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -137645,7 +137676,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -138191,7 +138223,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -140688,7 +140721,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -141234,7 +141268,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -141780,7 +141815,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -142326,7 +142362,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -142872,7 +142909,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -143418,7 +143456,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -143964,7 +144003,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -144510,7 +144550,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -145045,7 +145086,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -145580,7 +145622,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -146126,7 +146169,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -146672,7 +146716,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -149389,7 +149434,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -152111,7 +152157,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -152657,7 +152704,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -153437,7 +153485,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { @@ -154404,7 +154453,8 @@ "enum": [ "not_enabled", "not_effective", - "no_tool_results" + "no_tool_results", + "addon_unavailable" ] }, "createdAt": { diff --git a/docs/pages/platform-adding-llm-providers.md b/docs/pages/platform-adding-llm-providers.md index 65d10d6550b..ab9b5b0f0eb 100644 --- a/docs/pages/platform-adding-llm-providers.md +++ b/docs/pages/platform-adding-llm-providers.md @@ -3,7 +3,7 @@ title: Adding LLM Providers category: Development order: 2 description: Developer guide for implementing new LLM provider support in Archestra Platform -lastUpdated: 2026-04-29 +lastUpdated: 2026-07-11 --- @@ -171,10 +171,12 @@ The function must: 1. Iterate through provider-specific message array structure 2. Find tool result messages (e.g., `role: "tool"` in OpenAI, `tool_result` blocks in Anthropic, `functionResponse` parts in Gemini) -3. Parse JSON content and convert to TOON format using `@toon-format/toon` -4. Calculate token savings using the appropriate tokenizer +3. Batch the extracted results through `toonEncodeToolResults()` from `backend/src/routes/proxy/utils/toon-native.ts`. The native addon unwraps, parses, and encodes them — never encode TOON in the adapter +4. Get token counts to compare. For cl100k providers, pass a before-source to `toonEncodeToolResults()` and read the counts it returns; Anthropic and Bedrock count with their own tokenizer. Keep the TOON version only when it saves tokens 5. Return compressed messages and compression statistics +`toonEncodeToolResults()` returns `null` when the native addon is unavailable. The adapter must then keep the original results and report the `addon_unavailable` skip reason. + ### Metrics > **Note:** This is a known abstraction leak that we're planning to address in future versions. Thanks for bearing with us! diff --git a/docs/pages/platform-costs-and-limits.md b/docs/pages/platform-costs-and-limits.md index 9c11b5a6a7e..fd11503b166 100644 --- a/docs/pages/platform-costs-and-limits.md +++ b/docs/pages/platform-costs-and-limits.md @@ -2,7 +2,7 @@ title: Costs & Limits category: LLM Proxy order: 4 -lastUpdated: 2026-06-22 +lastUpdated: 2026-07-10 --- @@ -99,6 +99,7 @@ Compression is skipped when: - TOON is disabled - a response has no tool results - the TOON version would not save tokens +- the compression engine is unavailable (reported as `addon_unavailable`) Archestra records before/after token counts and savings when compression is applied, so those savings appear in logs and aggregate cost reporting. diff --git a/platform/.gitignore b/platform/.gitignore index f077dd40662..3f9413a552d 100644 --- a/platform/.gitignore +++ b/platform/.gitignore @@ -75,6 +75,7 @@ benchmark-config.env archestra-rs/sandbox-rs/index.js archestra-rs/app-runtime-rs/index.js archestra-rs/image-rs/index.js +archestra-rs/proxy-transform-rs/index.js # Playwright /playwright/ diff --git a/platform/Dockerfile b/platform/Dockerfile index 0f4ac9ac8d6..938221972bf 100644 --- a/platform/Dockerfile +++ b/platform/Dockerfile @@ -154,6 +154,7 @@ COPY archestra-rs/napi-loader/package.json archestra-rs/napi-loader/ COPY archestra-rs/sandbox-rs/package.json archestra-rs/sandbox-rs/ COPY archestra-rs/app-runtime-rs/package.json archestra-rs/app-runtime-rs/ COPY archestra-rs/image-rs/package.json archestra-rs/image-rs/ +COPY archestra-rs/proxy-transform-rs/package.json archestra-rs/proxy-transform-rs/ # Install all dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -195,6 +196,7 @@ COPY --from=deps /app/shared/node_modules ./shared/node_modules COPY --from=deps /app/archestra-rs/sandbox-rs/node_modules ./archestra-rs/sandbox-rs/node_modules COPY --from=deps /app/archestra-rs/app-runtime-rs/node_modules ./archestra-rs/app-runtime-rs/node_modules COPY --from=deps /app/archestra-rs/image-rs/node_modules ./archestra-rs/image-rs/node_modules +COPY --from=deps /app/archestra-rs/proxy-transform-rs/node_modules ./archestra-rs/proxy-transform-rs/node_modules # Copy source files COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ @@ -209,6 +211,8 @@ COPY archestra-rs/app-runtime-core ./archestra-rs/app-runtime-core COPY archestra-rs/app-runtime-rs ./archestra-rs/app-runtime-rs COPY archestra-rs/image-core ./archestra-rs/image-core COPY archestra-rs/image-rs ./archestra-rs/image-rs +COPY archestra-rs/proxy-transform-core ./archestra-rs/proxy-transform-core +COPY archestra-rs/proxy-transform-rs ./archestra-rs/proxy-transform-rs # Build all workspace ENV NEXT_TELEMETRY_DISABLED=1 @@ -224,7 +228,7 @@ FROM builder-base AS rust-napi-musl-smoke RUN --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/root/.cargo/git \ --mount=type=cache,target=/app/archestra-rs/target \ - pnpm --filter @archestra/sandbox-rs --filter @archestra/app-runtime-rs --filter @archestra/image-rs check:musl + pnpm --filter @archestra/sandbox-rs --filter @archestra/app-runtime-rs --filter @archestra/image-rs --filter @archestra/proxy-transform-rs check:musl # <----- Builder stage -----> FROM builder-base AS builder @@ -259,7 +263,7 @@ RUN --mount=type=cache,target=/root/.cargo/registry \ # Assemble the production backend with `pnpm deploy` — pnpm's recommended # monorepo packaging step (https://pnpm.io/cli/deploy). It produces a # self-contained /prod/backend whose node_modules includes every workspace -# dependency (the three NAPI crates, @archestra/napi-loader, @archestra/shared) +# dependency (the four NAPI crates, @archestra/napi-loader, @archestra/shared) # alongside their transitive production deps, with each crate able to resolve # @archestra/napi-loader from a nested store. This replaces the old hand-rolled # COPY of each crate's *.node / index.cjs into a workspace-shaped, root-hoisted @@ -435,7 +439,7 @@ RUN rm -rf /root/.cache/node/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx # Copy the fully-assembled backend from the builder's `pnpm deploy` output. # /prod/backend contains backend/dist, scripts, drizzle.config.ts and the # migrations (scoped by the backend package's `files` field) plus a -# self-contained node_modules — including the three NAPI crates with their +# self-contained node_modules — including the four NAPI crates with their # compiled *.node addons and a resolvable @archestra/napi-loader. This single # tree replaces the previous per-crate .node/index.cjs copy choreography and # the runtime `pnpm install`. diff --git a/platform/archestra-rs/Cargo.lock b/platform/archestra-rs/Cargo.lock index db7c7a10e29..d4424296023 100644 --- a/platform/archestra-rs/Cargo.lock +++ b/platform/archestra-rs/Cargo.lock @@ -17,6 +17,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.103" @@ -101,6 +113,36 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -122,6 +164,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -146,6 +199,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.65" @@ -176,6 +235,58 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -254,6 +365,73 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -455,6 +633,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -741,6 +930,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -753,6 +953,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1062,7 +1268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error", + "quick-error 2.0.1", ] [[package]] @@ -1105,6 +1311,26 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1313,7 +1539,7 @@ dependencies = [ "napi-build", "napi-sys", "nohash-hasher", - "rustc-hash", + "rustc-hash 2.1.2", "tokio", ] @@ -1389,6 +1615,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1536,6 +1768,34 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.18.1" @@ -1582,6 +1842,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.0", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.14.4" @@ -1599,18 +1878,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.118", ] +[[package]] +name = "proxy_transform_core" +version = "0.1.0" +dependencies = [ + "criterion", + "itoa", + "napi", + "napi-derive", + "proptest", + "serde", + "serde_json", + "tiktoken-rs", + "toon-format", +] + +[[package]] +name = "proxy_transform_rs" +version = "0.1.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "proxy_transform_core", + "serde_json", +] + [[package]] name = "pxfm" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-error" version = "2.0.1" @@ -1628,7 +1939,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.2", "rustls 0.23.41", "socket2 0.6.4", "thiserror 2.0.18", @@ -1649,7 +1960,7 @@ dependencies = [ "lru-slab", "rand", "ring", - "rustc-hash", + "rustc-hash 2.1.2", "rustls 0.23.41", "rustls-pki-types", "slab", @@ -1723,6 +2034,35 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1867,6 +2207,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1998,6 +2344,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -2150,6 +2508,7 @@ version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -2403,6 +2762,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiktoken-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bstr", + "fancy-regex", + "lazy_static", + "regex", + "rustc-hash 1.1.0", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2413,6 +2787,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -2506,6 +2890,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toon-format" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f89570c1a68d73941f728cca32a4345b2ffca36667ad921af336c60309a3e7e" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "tower" version = "0.5.3" @@ -2641,6 +3037,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2689,6 +3091,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/platform/archestra-rs/Cargo.toml b/platform/archestra-rs/Cargo.toml index b9afcdbf19e..5118bc12aad 100644 --- a/platform/archestra-rs/Cargo.toml +++ b/platform/archestra-rs/Cargo.toml @@ -1,5 +1,14 @@ [workspace] -members = ["app-runtime-core", "app-runtime-rs", "image-core", "image-rs", "sandbox-core", "sandbox-rs"] +members = [ + "app-runtime-core", + "app-runtime-rs", + "image-core", + "image-rs", + "proxy-transform-core", + "proxy-transform-rs", + "sandbox-core", + "sandbox-rs", +] resolver = "2" # No LTO: for these NAPI shims it measured slower to build AND a larger binary diff --git a/platform/archestra-rs/proxy-transform-core/Cargo.toml b/platform/archestra-rs/proxy-transform-core/Cargo.toml new file mode 100644 index 00000000000..a78f16a4598 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "proxy_transform_core" +version = "0.1.0" +edition = "2024" +publish = false + +[features] +default = [] +napi = ["dep:napi", "dep:napi-derive"] + +[dependencies] +itoa = "1" +napi = { version = "3", optional = true } +napi-derive = { version = "3", optional = true } +# de traits for the borrowed JSON DOM (src/json.rs); derive only in tests +serde = "1" +serde_json = "1" +# cl100k_base tokenizer for the fused keep/reject token counting (src/tokenize.rs), +# byte-identical ranks to the JS `tiktoken` get_encoding("cl100k_base") the +# adapters used. Pinned exact — the rank table is the parity contract. +tiktoken-rs = "=0.7.0" + +[dev-dependencies] +criterion = "0.5" +proptest = "1" +# fixture (de)serialization in the golden-corpus test +serde = { version = "1", features = ["derive"] } +# preserve_order is oracle-only: the production parser builds its own ordered +# JsonObject, but tests compare against toon-format over serde_json::Value, +# which must see document key order. Keeping the feature out of [dependencies] +# drops indexmap from the production (napi) build. +serde_json = { version = "1", features = ["preserve_order"] } +# parity oracle for the encoder (differential + round-trip tests) — the +# production path uses only src/encode.rs +toon-format = { version = "0.5.0", default-features = false } + +[[bench]] +name = "toon_kernel" +harness = false diff --git a/platform/archestra-rs/proxy-transform-core/benches/toon_kernel.rs b/platform/archestra-rs/proxy-transform-core/benches/toon_kernel.rs new file mode 100644 index 00000000000..abd98e085ef --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/benches/toon_kernel.rs @@ -0,0 +1,177 @@ +//! Diagnostic Criterion bench for the pure TOON kernel (no NAPI boundary). +//! The pre-registered performance threshold is measured by the JS harness +//! (`platform/backend/src/routes/proxy/__bench__/`); this bench isolates the +//! Rust-side cost per payload size on the same synthetic shapes as that +//! harness's corpus builder (uniform SKU rows / nested objects / prose, with +//! text-block wrappers and Bedrock-style `unwrap: false` items mixed in). + +use criterion::{ + BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main, +}; +use proxy_transform_core::{BeforeSource, ToonEncodeItem, toon_encode_tool_results}; +use serde_json::{Value, json}; + +/// (label, payload bytes) per corpus; every corpus is a 10-item batch covering +/// one full mix cycle of the JS corpus builder. +const PAYLOAD_SIZES: [(&str, usize); 4] = [ + ("1KB", 1 << 10), + ("10KB", 10 << 10), + ("100KB", 100 << 10), + ("1MB", 1 << 20), +]; +const ITEMS_PER_BATCH: usize = 10; + +fn build_batch(payload_bytes: usize, seed: u64) -> Vec { + let mut rng = TinyRng::new(seed); + (0..ITEMS_PER_BATCH) + .map(|i| { + // Same mix cycle as corpus.ts: per 10 items, 6 uniform arrays, + // 2 non-array objects, 2 non-JSON prose; 2 JSON items wrapped in + // the `[{"type":"text","text":...}]` client wrapper. + let kind = match i % 10 { + 2 | 7 => PayloadKind::Object, + 4 | 9 => PayloadKind::NonJson, + _ => PayloadKind::Array, + }; + let wrapped = !matches!(kind, PayloadKind::NonJson) && matches!(i % 10, 6 | 7); + let mut payload = build_payload(kind, payload_bytes, &mut rng); + if wrapped { + payload = serde_json::to_string(&json!([{ "type": "text", "text": payload }])) + .expect("wrapper serializes"); + } + ToonEncodeItem { + id: format!("bench_{i}"), + raw_content: payload, + unwrap: if wrapped { true } else { i % 7 != 3 }, + } + }) + .collect() +} + +enum PayloadKind { + Array, + Object, + NonJson, +} + +const WORDS: [&str; 12] = [ + "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliet", + "kilo", "lima", +]; +const STATUSES: [&str; 4] = ["active", "pending", "archived", "failed"]; + +fn make_row(rng: &mut TinyRng, id: usize) -> Value { + json!({ + "id": id, + "sku": format!("SKU-{}", rng.below(1_000_000)), + "name": format!("{} {}", WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]), + "status": STATUSES[rng.below(STATUSES.len())], + "score": (rng.below(10_000) as f64) / 100.0, + "quantity": rng.below(500), + "active": rng.below(2) == 0, + "updatedAt": format!("2026-0{}-{:02}T12:00:00Z", 1 + rng.below(6), 1 + rng.below(28)), + }) +} + +fn build_payload(kind: PayloadKind, target_bytes: usize, rng: &mut TinyRng) -> String { + match kind { + PayloadKind::Array => { + let mut rows = Vec::new(); + let mut size = 2; + let mut id = 0; + while size < target_bytes { + let row = make_row(rng, id); + id += 1; + size += serde_json::to_string(&row).expect("row serializes").len() + 1; + rows.push(row); + } + serde_json::to_string(&Value::Array(rows)).expect("array serializes") + } + PayloadKind::Object => { + let mut entries = serde_json::Map::new(); + let mut size = 64; + let mut id = 0; + while size < target_bytes { + let key = format!("entry_{id}"); + let mut value = make_row(rng, id); + value["nested"] = json!({ + "tags": [WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]], + "depth": 2, + }); + size += serde_json::to_string(&value) + .expect("entry serializes") + .len() + + key.len() + + 4; + entries.insert(key, value); + id += 1; + } + serde_json::to_string(&json!({ + "meta": { "source": "bench", "version": 3, "total": id }, + "entries": entries, + })) + .expect("object serializes") + } + PayloadKind::NonJson => { + let mut parts = vec![format!("Tool run {} output:", rng.below(1000))]; + let mut size = parts[0].len(); + while size < target_bytes { + let word = WORDS[rng.below(WORDS.len())]; + size += word.len() + 1; + parts.push(word.to_string()); + } + parts.join(" ") + } + } +} + +fn bench_toon_kernel(c: &mut Criterion) { + let mut group = c.benchmark_group("toon_kernel"); + group.sample_size(20); + // Two variants per size: `encode` isolates the transform; `encode+count` + // adds the fused cl100k token counting. The delta is the native marginal + // cost of the counting that used to run as synchronous WASM calls on the + // Node event loop. + for (variant, count) in [ + ("encode", None), + ("encode+count", Some(BeforeSource::Normalized)), + ] { + for (label, payload_bytes) in PAYLOAD_SIZES { + let batch = build_batch(payload_bytes, 0x5eed ^ payload_bytes as u64); + let total_bytes: usize = batch.iter().map(|item| item.raw_content.len()).sum(); + group.throughput(Throughput::Bytes(total_bytes as u64)); + group.bench_with_input(BenchmarkId::new(variant, label), &batch, |b, batch| { + b.iter_batched( + || batch.clone(), + |items| black_box(toon_encode_tool_results(black_box(items), count)), + BatchSize::LargeInput, + ); + }); + } + } + group.finish(); +} + +#[derive(Clone, Copy)] +struct TinyRng(u64); + +impl TinyRng { + fn new(seed: u64) -> Self { + Self(seed) + } + + fn next(&mut self) -> u64 { + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1); + self.0 + } + + fn below(&mut self, upper: usize) -> usize { + ((self.next() >> 16) as usize) % upper + } +} + +criterion_group!(benches, bench_toon_kernel); +criterion_main!(benches); diff --git a/platform/archestra-rs/proxy-transform-core/src/encode.rs b/platform/archestra-rs/proxy-transform-core/src/encode.rs new file mode 100644 index 00000000000..3c19e5fdadd --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/src/encode.rs @@ -0,0 +1,917 @@ +//! TOON (spec v3) encoder, byte-compatible with `toon-format` 0.5.0 +//! `encode_default` (2-space indent, comma delimiter, key folding off), but +//! linear-time and low-allocation: a single pass over the borrowed +//! [`JsonValue`] DOM writing into one output buffer. The upstream crate stays +//! as a dev-dependency and is the parity oracle: the committed golden corpus +//! plus the differential property test in `tests/roundtrip_property.rs` pin +//! byte equality. +//! +//! Trust boundary: input values come from [`crate::json::parse_json`], so +//! floats are always finite — the crate's NaN/Infinity→null normalization is +//! unreachable here and not replicated. Its -0.0→0 normalization falls out of +//! the integer-collapse float path below. +//! +//! Deliberate divergence from the crate — the OUTPUT BUDGET: TOON expands +//! exponent-form numbers to full decimal literals (`1e300`, 5 JSON bytes, +//! becomes 301 output bytes), so a legal payload of such numbers amplifies +//! ~60x and a large tool result could force multi-GiB allocations (Rust +//! aborts the host process on allocation failure — `catch_unwind` cannot stop +//! that). Encoding therefore aborts with [`EncodeError::OutputBudgetExceeded`] +//! once the output would exceed `max(2 x input bytes, 16KiB)`; the pipeline +//! maps that to `encoded: None` (fail-open, original payload kept). +//! Semantically safe: an encoding at >= 2x the input bytes can never win the +//! downstream token comparison, and skipping is closer to the old npm path, +//! which emitted compact exponent forms instead of expanding. Scalar writes +//! are pre-checked with their exact size (the crossing write is refused +//! outright, so the buffer cannot double its capacity past the budget); loop +//! bodies keep a post-write check as backstop for small structural output. +//! The pipeline adds two more anti-amplification layers on top of this +//! per-item budget: an aggregate batch budget and a per-item input size cap +//! (see `lib.rs`). + +use std::collections::{HashMap, HashSet}; +use std::fmt::Write as _; + +use crate::json::{JsonObject, JsonValue}; + +/// Mirrors `toon-format`'s `MAX_DEPTH`. Note the crate's depth counter is not +/// "one per nesting level": list-item object fields jump it by 2-3, so deeply +/// nested list shapes can exceed it and must fail exactly like the crate. +const MAX_DEPTH: usize = 256; + +/// Output budget floor (see module docs): small inputs may legitimately expand +/// past 2x (indentation, `[N]` markers), so the cap only starts binding above +/// this many output bytes. Also reused by the batch-level aggregate budget in +/// `lib.rs`. +pub(crate) const OUTPUT_BUDGET_FLOOR: usize = 16 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum EncodeError { + /// The depth counter exceeded [`MAX_DEPTH`] (crate: `InvalidStructure`). + MaxDepthExceeded, + /// The output would exceed `max(2 x input bytes, 16KiB)` — deliberate + /// anti-amplification divergence from the crate (see module docs). + OutputBudgetExceeded, + /// std `Display` produced exponent notation for a float. Unreachable + /// (verified: Display always expands), but emitting it verbatim would + /// break byte parity with the crate's expansion, so it fails closed. + ExponentFloatFormat, + /// A container reached a primitive-only position. Unreachable — every call + /// site pre-checks — kept so a future bug fails closed like the crate's + /// `InvalidInput` instead of emitting corrupt output. + NonPrimitive, +} + +/// Encode a parsed JSON value as TOON, byte-identical to +/// `toon_format::encode_default` for every output within the anti-amplification +/// budget derived from `input_len` (the JSON text length; also used to presize +/// the output buffer). Scalar writes are pre-checked with their exact size, so +/// the accept set is precisely "final output length <= budget". +pub(crate) fn encode_to_toon( + value: &JsonValue<'_>, + input_len: usize, +) -> Result { + let budget = OUTPUT_BUDGET_FLOOR.max(input_len.saturating_mul(2)); + let mut encoder = Encoder { + out: String::with_capacity(input_len), + budget, + float_scratch: String::new(), + }; + encoder.value(value)?; + Ok(encoder.out) +} + +struct Encoder { + out: String, + /// Maximum output length; a write that would grow past it aborts the encode. + budget: usize, + /// Reused staging buffer for float formatting (exact-size budget pre-check + /// and trailing-zero trim happen here before the text reaches `out`). + float_scratch: String, +} + +impl Encoder { + fn value(&mut self, value: &JsonValue<'_>) -> Result<(), EncodeError> { + match value { + JsonValue::Array(arr) => self.array(None, arr, 0), + JsonValue::Object(obj) => self.object(obj, 0), + primitive => self.primitive(primitive), + } + } + + fn object(&mut self, obj: &JsonObject<'_>, depth: usize) -> Result<(), EncodeError> { + check_depth(depth)?; + for (i, (key, value)) in obj.iter().enumerate() { + if i > 0 { + self.out.push('\n'); + } + match value { + JsonValue::Array(arr) => self.array(Some(key), arr, depth)?, + JsonValue::Object(nested) => { + self.indent(depth); + self.key(key)?; + self.out.push(':'); + if !nested.is_empty() { + self.out.push('\n'); + self.object(nested, depth + 1)?; + } + } + primitive => { + self.indent(depth); + self.key(key)?; + self.out.push_str(": "); + self.primitive(primitive)?; + } + } + self.check_budget()?; + } + Ok(()) + } + + fn array( + &mut self, + key: Option<&str>, + arr: &[JsonValue<'_>], + depth: usize, + ) -> Result<(), EncodeError> { + check_depth(depth)?; + if arr.is_empty() { + self.array_header(key, 0, None, depth)?; + return Ok(()); + } + if let Some(fields) = tabular_fields(arr) { + self.array_header(key, arr.len(), Some(&fields), depth)?; + self.out.push('\n'); + self.tabular_rows(arr, &fields, depth + 1) + } else if arr.iter().all(is_primitive) { + self.primitive_array(key, arr, depth) + } else { + self.list_array(key, arr, depth) + } + } + + fn primitive_array( + &mut self, + key: Option<&str>, + arr: &[JsonValue<'_>], + depth: usize, + ) -> Result<(), EncodeError> { + self.array_header(key, arr.len(), None, depth)?; + self.out.push(' '); + for (i, value) in arr.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.primitive(value)?; + } + Ok(()) + } + + /// One tabular row per array element, indented at `row_depth`. Rows almost + /// always repeat the header's key order, so the fast path writes values in + /// storage order. Set-equal-but-reordered rows go through a field→column + /// index built once per array (rows x fields hash lookups, the crate's own + /// complexity — a per-cell linear key search would be rows x fields^2). + fn tabular_rows( + &mut self, + arr: &[JsonValue<'_>], + fields: &[&str], + row_depth: usize, + ) -> Result<(), EncodeError> { + let mut field_columns: Option> = None; + let mut row_cells: Vec> = Vec::new(); + for (i, row) in arr.iter().enumerate() { + // Guaranteed an object by `tabular_fields`; skip mirrors the crate. + let Some(obj) = row.as_object() else { continue }; + self.indent(row_depth); + if obj.iter().map(|(key, _)| key).eq(fields.iter().copied()) { + for (j, (_, value)) in obj.iter().enumerate() { + if j > 0 { + self.out.push(','); + } + self.primitive(value)?; + } + } else { + let columns = field_columns.get_or_insert_with(|| { + fields + .iter() + .enumerate() + .map(|(column, &field)| (field, column)) + .collect() + }); + row_cells.clear(); + row_cells.resize(fields.len(), None); + for (key, value) in obj.iter() { + if let Some(&column) = columns.get(key) { + row_cells[column] = Some(value); + } + } + for (j, cell) in row_cells.iter().enumerate() { + if j > 0 { + self.out.push(','); + } + match cell { + Some(value) => self.primitive(value)?, + // Unreachable (detection verified every field), but the + // crate writes null for missing cells. + None => self.push_checked("null")?, + } + } + } + if i < arr.len() - 1 { + self.out.push('\n'); + } + self.check_budget()?; + } + Ok(()) + } + + /// List layout (`- item` lines) for arrays that are neither tabular nor + /// all-primitive. + fn list_array( + &mut self, + key: Option<&str>, + arr: &[JsonValue<'_>], + depth: usize, + ) -> Result<(), EncodeError> { + self.array_header(key, arr.len(), None, depth)?; + self.out.push('\n'); + for (i, item) in arr.iter().enumerate() { + self.indent(depth + 1); + self.out.push('-'); + match item { + JsonValue::Array(inner) => { + self.out.push(' '); + self.array(None, inner, depth + 1)?; + } + JsonValue::Object(obj) => self.list_item_object(obj, depth)?, + primitive => { + self.out.push(' '); + self.primitive(primitive)?; + } + } + if i < arr.len() - 1 { + self.out.push('\n'); + } + self.check_budget()?; + } + Ok(()) + } + + /// Object as a list item: first field on the hyphen line, remaining fields + /// two levels below it; empty objects are a bare hyphen. The uneven depth + /// jumps (+2/+3) replicate the crate's layout exactly. + fn list_item_object(&mut self, obj: &JsonObject<'_>, depth: usize) -> Result<(), EncodeError> { + let mut entries = obj.iter(); + let Some((first_key, first_value)) = entries.next() else { + return Ok(()); + }; + self.out.push(' '); + match first_value { + JsonValue::Array(arr) => { + self.key(first_key)?; + if let Some(fields) = tabular_fields(arr) { + // Tabular rows of a first-field array sit at depth + 3 + // relative to this list's header (crate quirk, no depth + // check on this path). + self.array_header(None, arr.len(), Some(&fields), 0)?; + self.out.push('\n'); + self.tabular_rows(arr, &fields, depth + 3)?; + } else { + self.array(None, arr, depth + 2)?; + } + } + JsonValue::Object(nested) => { + self.key(first_key)?; + self.out.push(':'); + if !nested.is_empty() { + self.out.push('\n'); + self.object(nested, depth + 3)?; + } + } + primitive => { + self.key(first_key)?; + self.out.push_str(": "); + self.primitive(primitive)?; + } + } + for (key, value) in entries { + self.out.push('\n'); + self.indent(depth + 2); + match value { + JsonValue::Array(arr) => { + self.key(key)?; + self.array(None, arr, depth + 2)?; + } + JsonValue::Object(nested) => { + self.key(key)?; + self.out.push(':'); + if !nested.is_empty() { + self.out.push('\n'); + self.object(nested, depth + 3)?; + } + } + primitive => { + self.key(key)?; + self.out.push_str(": "); + self.primitive(primitive)?; + } + } + self.check_budget()?; + } + Ok(()) + } + + /// `key[N]:`, `[N]:` or `key[N]{f1,f2}:`. Indent is written only when a + /// key is present (crate behavior — keyless headers are always inline). + fn array_header( + &mut self, + key: Option<&str>, + len: usize, + fields: Option<&[&str]>, + depth: usize, + ) -> Result<(), EncodeError> { + if let Some(key) = key { + self.indent(depth); + self.key(key)?; + } + self.out.push('['); + self.write_int(len)?; + self.out.push(']'); + if let Some(fields) = fields { + self.out.push('{'); + for (i, &field) in fields.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.key(field)?; + } + self.out.push('}'); + } + self.out.push(':'); + Ok(()) + } + + fn primitive(&mut self, value: &JsonValue<'_>) -> Result<(), EncodeError> { + match value { + JsonValue::Null => self.push_checked("null"), + JsonValue::Bool(true) => self.push_checked("true"), + JsonValue::Bool(false) => self.push_checked("false"), + // Integer typing follows the parse (see crate::json); formatting + // matches the crate's as_i64 → as_u64 fallback digit-for-digit. + JsonValue::PosInt(u) => self.write_int(*u), + JsonValue::NegInt(i) => self.write_int(*i), + JsonValue::Float(f) => self.f64_canonical(*f), + JsonValue::String(s) => self.string_value(s), + JsonValue::Array(_) | JsonValue::Object(_) => Err(EncodeError::NonPrimitive), + } + } + + /// Append `text` only if the result stays within the output budget; the + /// crossing write is refused BEFORE it lands, so the buffer never grows + /// (or reallocates) past the budget on a scalar. `len + text.len() > + /// budget` is exactly the post-write condition, so the accept set is + /// unchanged versus checking afterwards. + fn push_checked(&mut self, text: &str) -> Result<(), EncodeError> { + if self.out.len() + text.len() > self.budget { + return Err(EncodeError::OutputBudgetExceeded); + } + self.out.push_str(text); + Ok(()) + } + + /// Backstop for the small structural output (indent, punctuation, `- `) + /// that is not routed through [`Self::push_checked`]. Called at the end of + /// every object entry / array row / list item. + fn check_budget(&self) -> Result<(), EncodeError> { + if self.out.len() > self.budget { + return Err(EncodeError::OutputBudgetExceeded); + } + Ok(()) + } + + /// Canonical TOON float formatting, mirroring the crate's + /// `format_canonical_number` for `Number::Float`: + /// 1. integer-valued floats in i64/u64 range print as integers (this also + /// turns -0.0 into "0", covering the crate's normalize step). Note the + /// crate's `as_i64` saturating-cast check has no `i64::MAX` exclusion, + /// so the float 2^63 prints as 9223372036854775807 (and, via the same + /// saturation in `as_u64`, 2^64 prints as u64::MAX) — replicated, the + /// boundary parity test pins it; + /// 2. everything else is std `Display` (shortest repr, and — verified — + /// never exponent notation, so the crate's exponent-expansion fallback + /// is unreachable) with the crate's trailing-zero trim applied. + fn f64_canonical(&mut self, f: f64) -> Result<(), EncodeError> { + let i = f as i64; + if i as f64 == f { + return self.write_int(i); + } + if f >= 0.0 { + let u = f as u64; + if u as f64 == f { + return self.write_int(u); + } + } + // Stage the Display text in the reused scratch buffer so the budget + // pre-check sees the exact final size. + let mut scratch = std::mem::take(&mut self.float_scratch); + scratch.clear(); + let _ = write!(scratch, "{f}"); + // Fail closed if std Display ever produced exponent notation: the + // crate expands exponents, so emitting this verbatim would break byte + // parity, and the trim below would mangle it. Unreachable today + // (verified empirically), guarded at runtime so a release build + // returns None instead of corrupt bytes. + let result = if scratch.contains(['e', 'E']) { + Err(EncodeError::ExponentFloatFormat) + } else { + trim_trailing_zeros(&mut scratch); + self.push_checked(&scratch) + }; + self.float_scratch = scratch; + result + } + + fn string_value(&mut self, s: &str) -> Result<(), EncodeError> { + if needs_quoting(s) { + self.quoted(s) + } else { + self.push_checked(s) + } + } + + fn key(&mut self, key: &str) -> Result<(), EncodeError> { + if is_valid_unquoted_key(key) { + self.push_checked(key) + } else { + self.quoted(key) + } + } + + /// Quoted string: only `\n \r \t " \` are escaped; every other character + /// (including other control characters) passes through raw. The exact + /// escaped size (each escape adds one byte, plus two quotes) is checked + /// against the budget before anything is written. + fn quoted(&mut self, s: &str) -> Result<(), EncodeError> { + let escapes = s + .bytes() + .filter(|b| matches!(b, b'\n' | b'\r' | b'\t' | b'"' | b'\\')) + .count(); + if self.out.len() + s.len() + escapes + 2 > self.budget { + return Err(EncodeError::OutputBudgetExceeded); + } + self.out.push('"'); + let mut plain_from = 0; + for (i, b) in s.bytes().enumerate() { + let escaped = match b { + b'\n' => "\\n", + b'\r' => "\\r", + b'\t' => "\\t", + b'"' => "\\\"", + b'\\' => "\\\\", + _ => continue, + }; + self.out.push_str(&s[plain_from..i]); + self.out.push_str(escaped); + plain_from = i + 1; + } + self.out.push_str(&s[plain_from..]); + self.out.push('"'); + Ok(()) + } + + fn indent(&mut self, depth: usize) { + const CHUNK: &str = " "; // 32 spaces + let mut remaining = depth * 2; + while remaining >= CHUNK.len() { + self.out.push_str(CHUNK); + remaining -= CHUNK.len(); + } + self.out.push_str(&CHUNK[..remaining]); + } + + fn write_int(&mut self, value: impl itoa::Integer) -> Result<(), EncodeError> { + let mut buf = itoa::Buffer::new(); + self.push_checked(buf.format(value)) + } +} + +/// Crate's `remove_trailing_zeros`: trim trailing zeros after a single decimal +/// point, dropping the point when the fraction empties. (Shortest-repr Display +/// output makes this a no-op, but the crate applies it to every non-integer +/// float, so we mirror it.) +fn trim_trailing_zeros(text: &mut String) { + let end = { + let bytes = text.as_bytes(); + let Some(dot) = bytes.iter().position(|&b| b == b'.') else { + return; + }; + if bytes[dot + 1..].contains(&b'.') { + return; // multiple dots: crate returns the string unchanged + } + let mut end = bytes.len(); + while end > dot + 1 && bytes[end - 1] == b'0' { + end -= 1; + } + if end == dot + 1 { dot } else { end } + }; + text.truncate(end); +} + +fn check_depth(depth: usize) -> Result<(), EncodeError> { + if depth > MAX_DEPTH { + return Err(EncodeError::MaxDepthExceeded); + } + Ok(()) +} + +fn is_primitive(value: &JsonValue<'_>) -> bool { + !matches!(value, JsonValue::Array(_) | JsonValue::Object(_)) +} + +/// Tabular detection: every element is an object with only primitive values, +/// the same field count as the first element, and (order-insensitively) the +/// first element's field set. Returns the first element's fields in order. +/// +/// Order-insensitive comparison is O(total fields): a field set is built once +/// per array (lazily, on the first out-of-order row) and each row's keys are +/// checked against it. Row keys are unique (the parser deduplicates) and the +/// counts match, so "every row key is a header field" is exactly the crate's +/// "every header field is present in the row". +fn tabular_fields<'v>(arr: &'v [JsonValue<'_>]) -> Option> { + let first = arr.first()?.as_object()?; + if !first.iter().all(|(_, value)| is_primitive(value)) { + return None; + } + let fields: Vec<&str> = first.iter().map(|(key, _)| key).collect(); + let mut field_set: Option> = None; + for row in &arr[1..] { + let obj = row.as_object()?; + if obj.len() != fields.len() { + return None; + } + // Fast path: identical field order; otherwise compare key sets. + if !obj.iter().map(|(key, _)| key).eq(fields.iter().copied()) { + let set = field_set.get_or_insert_with(|| fields.iter().copied().collect()); + if !obj.iter().all(|(key, _)| set.contains(key)) { + return None; + } + } + if !obj.iter().all(|(_, value)| is_primitive(value)) { + return None; + } + } + Some(fields) +} + +/// A string value can stay unquoted only if it cannot be misread as a literal, +/// a number, or structure. Comma is the only delimiter we emit, so the crate's +/// object/array quoting contexts collapse into one predicate. +fn needs_quoting(s: &str) -> bool { + if s.is_empty() { + return true; + } + if matches!(s, "null" | "true" | "false") { + return true; + } + if is_numeric_like(s) { + return true; + } + // Structural chars (note: '-' anywhere), escapes, delimiter, escaped + // whitespace — all single ASCII bytes, so a byte scan is exact on UTF-8. + if s.bytes().any(|b| { + matches!( + b, + b'[' | b']' | b'{' | b'}' | b':' | b'-' | b'\\' | b'"' | b',' | b'\n' | b'\r' | b'\t' + ) + }) { + return true; + } + if s.starts_with(char::is_whitespace) || s.ends_with(char::is_whitespace) { + return true; + } + // Leading zero followed by a digit reads as a malformed number. + s.starts_with('0') && s[1..].starts_with(|c: char| c.is_ascii_digit()) +} + +/// Crate's `is_numeric_like`: optional leading '-', then a digit (no leading +/// zeros), then only `[0-9.eE+-]`. Byte-level scan is exact: every significant +/// character is ASCII and multi-byte UTF-8 units never match ASCII patterns. +fn is_numeric_like(s: &str) -> bool { + let digits = s.strip_prefix('-').unwrap_or(s).as_bytes(); + let Some(&first) = digits.first() else { + return false; + }; + if !first.is_ascii_digit() { + return false; + } + if first == b'0' && digits.get(1).is_some_and(u8::is_ascii_digit) { + return false; + } + digits + .iter() + .all(|&b| b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-')) +} + +/// Unquoted keys: alphabetic (Unicode) or '_' first, then alphanumeric +/// (Unicode), '_' or '.'. +fn is_valid_unquoted_key(key: &str) -> bool { + let mut chars = key.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !first.is_alphabetic() && first != '_' { + return false; + } + chars.all(|c| c.is_alphanumeric() || c == '_' || c == '.') +} + +#[cfg(test)] +mod tests { + use std::fmt::Write as _; + + use crate::json::parse_json; + + use super::*; + + fn encode(raw: &str) -> String { + let value = parse_json(raw).expect("test input parses"); + encode_to_toon(&value, raw.len()).expect("encodes") + } + + #[test] + fn quoting_rules() { + for quoted in [ + "", "null", "true", "false", "123", "0", "-5", "3.14", "1e10", "0.5", "07", "a-b", + "-x", "a,b", "a:b", "a[b", "a]b", "a{b", "a}b", "a\"b", "a\\b", "a\nb", "a\rb", "a\tb", + " x", "x ", "\u{a0}x", "x\u{a0}", "1.2.3", + ] { + assert!(needs_quoting(quoted), "expected quoting for {quoted:?}"); + } + for unquoted in ["hello", "hello world", "0x", "+5", "1a2", "a.b", "x0", "é"] { + assert!( + !needs_quoting(unquoted), + "expected no quoting for {unquoted:?}" + ); + } + } + + #[test] + fn key_rules() { + for ok in ["a", "_", "key.name.sub", "key.", "Ключ", "k1_2.x"] { + assert!(is_valid_unquoted_key(ok), "expected unquoted key {ok:?}"); + } + for quoted in ["", "1a", ".a", "a-b", "a b", "a:b", "a[b]"] { + assert!( + !is_valid_unquoted_key(quoted), + "expected quoted key {quoted:?}" + ); + } + } + + #[test] + fn float_formatting_matches_canonical_rules() { + for (raw, expected) in [ + (r#"{"x": 1.0}"#, "x: 1"), + (r#"{"x": -0.0}"#, "x: 0"), + (r#"{"x": -0}"#, "x: 0"), + (r#"{"x": 0.1}"#, "x: 0.1"), + (r#"{"x": 1e-7}"#, "x: 0.0000001"), + (r#"{"x": 1.5}"#, "x: 1.5"), + // 2^63: the crate's saturating as_i64 cast prints i64::MAX here + // (documented parity quirk, see f64_canonical). + (r#"{"x": 9.223372036854776e18}"#, "x: 9223372036854775807"), + (r#"{"x": -9.223372036854776e18}"#, "x: -9223372036854775808"), + ] { + assert_eq!(encode(raw), expected, "raw: {raw}"); + } + let huge = encode(r#"{"x": 1e300}"#); + assert!(huge.starts_with("x: 1")); + assert_eq!(huge.len(), "x: ".len() + 301); + } + + #[test] + fn empty_object_array_quirk() { + assert_eq!(encode("[{}, {}]"), "[2]{}:\n \n "); + assert_eq!(encode(r#"{"a": [{}]}"#), "a[1]{}:\n "); + } + + #[test] + fn structure_layouts() { + assert_eq!(encode("{}"), ""); + assert_eq!(encode("[]"), "[0]:"); + assert_eq!(encode(r#"{"a": {}}"#), "a:"); + assert_eq!(encode(r#"[1, "two", null]"#), "[3]: 1,two,null"); + assert_eq!( + encode(r#"{"users": [{"id": 1, "n": "a"}, {"id": 2, "n": "b"}]}"#), + "users[2]{id,n}:\n 1,a\n 2,b" + ); + assert_eq!( + encode(r#"[{"rows": [{"id": 1}], "total": 1}, "tail"]"#), + "[2]:\n - rows[1]{id}:\n 1\n total: 1\n - tail" + ); + assert_eq!( + encode(r#"[[1, 2], {"a": {"b": 1}}]"#), + "[2]:\n - [2]: 1,2\n - a:\n b: 1" + ); + } + + #[test] + fn reordered_tabular_rows_use_header_field_order() { + assert_eq!( + encode(r#"[{"a": 1, "b": 2}, {"b": 4, "a": 3}]"#), + "[2]{a,b}:\n 1,2\n 3,4" + ); + } + + /// Adversarial reordered-tabular shape (10k rows x 30 keys, odd rows in + /// reversed key order): output must match the crate byte-for-byte, and the + /// field→column index keeps it rows x keys instead of rows x keys^2. + #[test] + fn reordered_tabular_rows_match_crate_at_scale() { + const ROWS: usize = 10_000; + const KEYS: usize = 30; + let mut json = String::from("["); + for row in 0..ROWS { + if row > 0 { + json.push(','); + } + json.push('{'); + let order: Vec = if row % 2 == 0 { + (0..KEYS).collect() + } else { + (0..KEYS).rev().collect() + }; + for (j, k) in order.into_iter().enumerate() { + if j > 0 { + json.push(','); + } + let _ = write!(json, r#""k{k:02}":{}"#, row * 31 + k); + } + json.push('}'); + } + json.push(']'); + + let ours = encode(&json); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("oracle parse"); + let expected = toon_format::encode_default(&parsed).expect("oracle encodes"); + assert_eq!(ours, expected); + } + + /// Detection stress: 2 rows x 5k keys with the second row fully reversed. + /// Set-based detection must stay O(total fields) — a per-field linear + /// lookup would be 25M comparisons here — and the wide objects also + /// exercise the parser's large-object duplicate index. + #[test] + fn wide_reversed_tabular_detection_matches_crate() { + const KEYS: usize = 5_000; + let mut json = String::from("[{"); + for k in 0..KEYS { + if k > 0 { + json.push(','); + } + let _ = write!(json, r#""k{k:04}":{k}"#); + } + json.push_str("},{"); + for (j, k) in (0..KEYS).rev().enumerate() { + if j > 0 { + json.push(','); + } + let _ = write!(json, r#""k{k:04}":{k}"#); + } + json.push_str("}]"); + + let ours = encode(&json); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("oracle parse"); + let expected = toon_format::encode_default(&parsed).expect("oracle encodes"); + assert_eq!(ours, expected); + } + + /// Targeted i64/u64/f64 boundary parity against the crate. + #[test] + fn boundary_number_parity_with_crate() { + for raw in [ + "-9223372036854775808", // i64::MIN + "9223372036854775807", // i64::MAX + "18446744073709551615", // u64::MAX + "9007199254740991", // 2^53 - 1 + "9007199254740992", // 2^53 + "9007199254740993", // 2^53 + 1 (exact as i64) + "9007199254740993.0", // 2^53 + 1 as float (rounds to 2^53) + "9223372036854775808.0", // 2^63 as float (crate as_i64 saturation) + "18446744073709551616.0", // 2^64 as float (crate as_u64 saturation) + "-9223372036854775808.0", // -2^63 as float (i64 path) + "5e-324", // smallest subnormal + "2.2250738585072014e-308", // smallest normal + "1.7976931348623157e308", // f64::MAX + "-0.0", + "-0", + "0.1", + "1e-7", + "1e300", + "-1e300", + "1.0", + "-1.0", + "3.141592653589793", + ] { + let ours = encode(raw); + let parsed: serde_json::Value = serde_json::from_str(raw).expect("oracle parse"); + let expected = toon_format::encode_default(&parsed).expect("oracle encodes"); + assert_eq!(ours, expected, "raw: {raw}"); + } + } + + #[test] + fn exponent_amplification_aborts_within_budget() { + // ~72KB of exponent-form numbers would expand ~50x (each 1e300 is 5 + // JSON bytes but 301 output bytes); the encode must abort instead. + let large = format!("[{}]", vec!["1e300"; 12_000].join(",")); + let value = parse_json(&large).expect("parses"); + assert_eq!( + encode_to_toon(&value, large.len()), + Err(EncodeError::OutputBudgetExceeded) + ); + } + + #[test] + fn output_budget_boundary_encodes_below_and_aborts_above() { + // Small inputs bind at the 16KiB floor: 50 x 1e300 -> ~15.1KB output + // (still byte-identical to the crate), 60 x -> ~18.1KB trips it. + let under = format!("[{}]", vec!["1e300"; 50].join(",")); + let parsed: serde_json::Value = serde_json::from_str(&under).expect("oracle parse"); + assert_eq!( + encode(&under), + toon_format::encode_default(&parsed).expect("oracle encodes") + ); + + let over = format!("[{}]", vec!["1e300"; 60].join(",")); + let value = parse_json(&over).expect("parses"); + assert_eq!( + encode_to_toon(&value, over.len()), + Err(EncodeError::OutputBudgetExceeded) + ); + let parsed: serde_json::Value = serde_json::from_str(&over).expect("oracle parse"); + let crate_len = toon_format::encode_default(&parsed) + .expect("oracle encodes") + .len(); + assert!( + crate_len > OUTPUT_BUDGET_FLOOR.max(2 * over.len()), + "the skipped output must genuinely exceed the budget" + ); + } + + #[test] + fn budget_abort_keeps_buffer_bounded() { + // 100k exponent floats would expand to ~30MB; the encoder must stop + // at the budget instead of building the whole string. Scalar writes + // are pre-checked, so the length never exceeds the budget at all, and + // capacity growth (amortized doubling) stays within 2x of it. + let numbers = vec![JsonValue::Float(1e300); 100_000]; + let mut encoder = Encoder { + out: String::new(), + budget: OUTPUT_BUDGET_FLOOR, + float_scratch: String::new(), + }; + assert_eq!( + encoder.array(None, &numbers, 0), + Err(EncodeError::OutputBudgetExceeded) + ); + assert!( + encoder.out.len() <= OUTPUT_BUDGET_FLOOR, + "len={}", + encoder.out.len() + ); + assert!( + encoder.out.capacity() <= 2 * OUTPUT_BUDGET_FLOOR, + "capacity={}", + encoder.out.capacity() + ); + } + + #[test] + fn deep_list_nesting_exceeds_max_depth() { + // Built by hand: through `parse_json` the serde_json 128-level + // recursion limit rejects the document first, so encode-side depth + // failure only guards manually constructed values — exactly like the + // crate, which we assert as the oracle. + let mut value = JsonValue::String("leaf".into()); + let mut oracle = serde_json::json!("leaf"); + for _ in 0..200 { + let mut obj = JsonObject::default(); + obj.push_for_tests("first", JsonValue::PosInt(1)); + obj.push_for_tests("k", value); + value = JsonValue::Array(vec![JsonValue::Object(obj)]); + oracle = serde_json::json!([{ "first": 1, "k": oracle }]); + } + // Large input_len: the output budget must not fire before the depth + // check does (there is no real input text for a hand-built DOM). + assert_eq!( + encode_to_toon(&value, 1 << 20), + Err(EncodeError::MaxDepthExceeded) + ); + assert!( + toon_format::encode_default(&oracle).is_err(), + "oracle must reject the same depth" + ); + } +} diff --git a/platform/archestra-rs/proxy-transform-core/src/json.rs b/platform/archestra-rs/proxy-transform-core/src/json.rs new file mode 100644 index 00000000000..675be929081 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/src/json.rs @@ -0,0 +1,369 @@ +//! Minimal borrowed JSON DOM for the TOON kernel. `serde_json::Value` was the +//! hot spot of the pipeline (per-key IndexMap hashing plus an owned `String` +//! per key and string); this DOM parses ~3x faster by borrowing every +//! escape-free string/key from the input buffer and storing object entries in +//! insertion order in a plain `Vec`. +//! +//! Parity contract (the encoder's byte-parity oracle parses with +//! `serde_json::Value`, so this DOM must be observationally identical): +//! - numbers keep `serde_json`'s exact typing — the deserializer picks +//! `visit_u64`/`visit_i64`/`visit_f64` and we store what it hands us; +//! - duplicate object keys replicate `IndexMap::insert`, which JS `JSON.parse` +//! also matches: the key keeps its first position, the value is replaced; +//! - `parse_json` fails on trailing content (like `serde_json::from_str`) and +//! inherits `serde_json`'s 128-level recursion limit. +//! +//! Memory bound: strings and keys containing escapes force owned `Cow` +//! allocations (an input of millions of tiny escaped strings means millions of +//! tiny allocations), and every enum slot costs DOM overhead. Both are bounded +//! by the pipeline's per-item input cap (`MAX_ITEM_INPUT_BYTES` in `lib.rs`): +//! oversized items are never parsed at all. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::fmt; + +use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor}; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum JsonValue<'a> { + Null, + Bool(bool), + /// Non-negative integer literal (`visit_u64`). + PosInt(u64), + /// Negative integer literal (`visit_i64`). + NegInt(i64), + /// Everything else, always finite (`visit_f64`). + Float(f64), + String(Cow<'a, str>), + Array(Vec>), + Object(JsonObject<'a>), +} + +impl<'a> JsonValue<'a> { + pub(crate) fn as_object(&self) -> Option<&JsonObject<'a>> { + match self { + JsonValue::Object(obj) => Some(obj), + _ => None, + } + } + + pub(crate) fn as_str(&self) -> Option<&str> { + match self { + JsonValue::String(text) => Some(text), + _ => None, + } + } +} + +/// Object entries in first-occurrence order. +#[derive(Clone, Debug, PartialEq, Default)] +pub(crate) struct JsonObject<'a> { + entries: Vec<(Cow<'a, str>, JsonValue<'a>)>, +} + +impl<'a> JsonObject<'a> { + pub(crate) fn len(&self) -> usize { + self.entries.len() + } + + pub(crate) fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub(crate) fn iter(&self) -> impl ExactSizeIterator)> { + self.entries + .iter() + .map(|(key, value)| (key.as_ref(), value)) + } + + /// Linear-scan lookup: only used on cold paths (wrapper detection on a + /// first element, reordered tabular rows), never per-key on hot loops. + pub(crate) fn get(&self, key: &str) -> Option<&JsonValue<'a>> { + self.entries + .iter() + .find(|(entry_key, _)| entry_key == key) + .map(|(_, value)| value) + } + + pub(crate) fn get_mut(&mut self, key: &str) -> Option<&mut JsonValue<'a>> { + self.entries + .iter_mut() + .find(|(entry_key, _)| entry_key == key) + .map(|(_, value)| value) + } + + /// Direct entry construction for tests that need values deeper than the + /// parser's recursion limit. Skips duplicate handling. + #[cfg(test)] + pub(crate) fn push_for_tests(&mut self, key: &'a str, value: JsonValue<'a>) { + self.entries.push((Cow::Borrowed(key), value)); + } +} + +/// Parse a complete JSON document, rejecting trailing content — the same +/// accept set as `serde_json::from_str::`. +pub(crate) fn parse_json(input: &str) -> Option> { + let mut deserializer = serde_json::Deserializer::from_str(input); + let value = ValueSeed.deserialize(&mut deserializer).ok()?; + deserializer.end().ok()?; + Some(value) +} + +// === parsing internals === + +struct ValueSeed; + +impl<'de> DeserializeSeed<'de> for ValueSeed { + type Value = JsonValue<'de>; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_any(ValueSeed) + } +} + +impl<'de> Visitor<'de> for ValueSeed { + type Value = JsonValue<'de>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("any JSON value") + } + + fn visit_unit(self) -> Result { + Ok(JsonValue::Null) + } + + fn visit_bool(self, value: bool) -> Result { + Ok(JsonValue::Bool(value)) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(JsonValue::PosInt(value)) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(JsonValue::NegInt(value)) + } + + fn visit_f64(self, value: f64) -> Result { + Ok(JsonValue::Float(value)) + } + + fn visit_borrowed_str(self, value: &'de str) -> Result { + Ok(JsonValue::String(Cow::Borrowed(value))) + } + + fn visit_str(self, value: &str) -> Result { + Ok(JsonValue::String(Cow::Owned(value.to_owned()))) + } + + fn visit_string(self, value: String) -> Result { + Ok(JsonValue::String(Cow::Owned(value))) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut items = Vec::new(); + while let Some(item) = seq.next_element_seed(ValueSeed)? { + items.push(item); + } + Ok(JsonValue::Array(items)) + } + + fn visit_map>(self, mut map: A) -> Result { + let mut builder = ObjectBuilder::default(); + while let Some(key) = map.next_key_seed(KeySeed)? { + let value = map.next_value_seed(ValueSeed)?; + builder.insert(key, value); + } + Ok(JsonValue::Object(JsonObject { + entries: builder.entries, + })) + } +} + +struct KeySeed; + +impl<'de> DeserializeSeed<'de> for KeySeed { + type Value = Cow<'de, str>; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_str(KeySeed) + } +} + +impl<'de> Visitor<'de> for KeySeed { + type Value = Cow<'de, str>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an object key") + } + + fn visit_borrowed_str(self, value: &'de str) -> Result { + Ok(Cow::Borrowed(value)) + } + + fn visit_str(self, value: &str) -> Result { + Ok(Cow::Owned(value.to_owned())) + } + + fn visit_string(self, value: String) -> Result { + Ok(Cow::Owned(value)) + } +} + +/// Above this size, duplicate detection switches from a linear scan to a lazy +/// hash index so pathological many-key objects stay O(n). +const LINEAR_DEDUP_MAX: usize = 32; + +#[derive(Default)] +struct ObjectBuilder<'a> { + entries: Vec<(Cow<'a, str>, JsonValue<'a>)>, + /// Key → entry index, built lazily for large objects. SipHash (std + /// default) keeps adversarial key sets collision-resistant. Map keys are + /// clones of the entry keys: free for borrowed keys (the common case), + /// one String copy for escaped ones. + index: Option, usize>>, +} + +impl<'a> ObjectBuilder<'a> { + /// `IndexMap::insert` semantics: an existing key keeps its position and + /// gets the new value; a new key is appended. + fn insert(&mut self, key: Cow<'a, str>, value: JsonValue<'a>) { + if self.index.is_none() && self.entries.len() >= LINEAR_DEDUP_MAX { + self.index = Some( + self.entries + .iter() + .enumerate() + .map(|(i, (existing, _))| (existing.clone(), i)) + .collect(), + ); + } + match &mut self.index { + Some(index) => match index.entry(key.clone()) { + Entry::Occupied(slot) => { + self.entries[*slot.get()].1 = value; + } + Entry::Vacant(slot) => { + slot.insert(self.entries.len()); + self.entries.push((key, value)); + } + }, + None => { + if let Some(entry) = self + .entries + .iter_mut() + .find(|(existing, _)| *existing == key) + { + entry.1 = value; + return; + } + self.entries.push((key, value)); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_all_value_kinds() { + let value = parse_json(r#"{"a":null,"b":true,"c":18446744073709551615,"d":-3,"e":1.5,"f":"x\ny","g":[1],"h":{}}"#) + .expect("parses"); + let JsonValue::Object(obj) = value else { + panic!("expected object"); + }; + assert_eq!(obj.get("a"), Some(&JsonValue::Null)); + assert_eq!(obj.get("b"), Some(&JsonValue::Bool(true))); + assert_eq!(obj.get("c"), Some(&JsonValue::PosInt(u64::MAX))); + assert_eq!(obj.get("d"), Some(&JsonValue::NegInt(-3))); + assert_eq!(obj.get("e"), Some(&JsonValue::Float(1.5))); + assert_eq!( + obj.get("f"), + Some(&JsonValue::String(Cow::Owned("x\ny".to_string()))) + ); + assert_eq!( + obj.get("g"), + Some(&JsonValue::Array(vec![JsonValue::PosInt(1)])) + ); + assert_eq!( + obj.get("h"), + Some(&JsonValue::Object(JsonObject::default())) + ); + } + + #[test] + fn escape_free_strings_borrow_from_input() { + let input = r#"{"key":"plain value"}"#; + let value = parse_json(input).expect("parses"); + let JsonValue::Object(obj) = value else { + panic!("expected object"); + }; + let Some(JsonValue::String(text)) = obj.get("key") else { + panic!("expected string"); + }; + assert!(matches!(text, Cow::Borrowed(_))); + } + + #[test] + fn duplicate_keys_keep_first_position_and_last_value() { + // Same semantics as serde_json's preserve_order IndexMap and JS + // JSON.parse: first position wins, last value wins. + let value = parse_json(r#"{"a":1,"b":2,"a":3}"#).expect("parses"); + let JsonValue::Object(obj) = value else { + panic!("expected object"); + }; + let entries: Vec<_> = obj.iter().collect(); + assert_eq!( + entries, + vec![("a", &JsonValue::PosInt(3)), ("b", &JsonValue::PosInt(2))] + ); + } + + #[test] + fn duplicate_keys_dedup_in_large_objects() { + let mut input = String::from("{"); + for i in 0..100 { + input.push_str(&format!(r#""k{i}":{i},"#)); + } + input.push_str(r#""k7":777}"#); + let value = parse_json(&input).expect("parses"); + let JsonValue::Object(obj) = value else { + panic!("expected object"); + }; + assert_eq!(obj.len(), 100); + assert_eq!(obj.get("k7"), Some(&JsonValue::PosInt(777))); + assert_eq!(obj.iter().nth(7), Some(("k7", &JsonValue::PosInt(777)))); + } + + #[test] + fn rejects_trailing_content_and_invalid_json() { + for bad in [ + "{} garbage", + "[1,2] 3", + "", + "{'a':1}", + "[1,", + "NaN", + "1e400", + ] { + assert!(parse_json(bad).is_none(), "expected reject: {bad:?}"); + } + } + + #[test] + fn respects_serde_json_recursion_limit() { + let deep_ok = format!("{}1{}", "[".repeat(127), "]".repeat(127)); + assert!(parse_json(&deep_ok).is_some()); + let too_deep = format!("{}1{}", "[".repeat(129), "]".repeat(129)); + assert!(parse_json(&too_deep).is_none()); + } +} diff --git a/platform/archestra-rs/proxy-transform-core/src/lib.rs b/platform/archestra-rs/proxy-transform-core/src/lib.rs new file mode 100644 index 00000000000..b19ec91edc2 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/src/lib.rs @@ -0,0 +1,596 @@ +//! Pure tool-result transformation kernel for the LLM proxy: unwrap the text-block +//! wrapper some clients (n8n, Vercel AI SDK) add around tool results, parse the +//! JSON, and encode it as TOON (spec v3, own linear encoder in [`encode`], +//! byte-compatible with the `toon-format` crate, which remains the test oracle). +//! +//! Node-free; the NAPI adapter lives in `proxy_transform_rs`. Per-item processing +//! is infallible: content that is not parseable JSON yields `encoded: None` and the +//! adapter keeps the original payload (fail-open, exactly like the TS path today). +//! +//! Anti-amplification limits (all fail-open to `encoded: None`): a per-item +//! output budget (see [`encode`] module docs), an aggregate batch output budget +//! (see [`toon_encode_tool_results`]), and a per-item input size cap +//! ([`MAX_ITEM_INPUT_BYTES`]). + +mod encode; +mod json; +mod tokenize; + +use json::JsonValue; + +/// One tool result to transform. `id` is the provider tool id, carried for +/// logging only — it is not unique across items (Anthropic reuses one +/// `tool_use_id` across blocks), so results are matched to inputs by position. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "napi", napi_derive::napi(object))] +pub struct ToonEncodeItem { + pub id: String, + #[cfg_attr(feature = "napi", napi(js_name = "rawContent"))] + pub raw_content: String, + pub unwrap: bool, +} + +/// Which string an adapter tokenizes as the pre-compression baseline, selecting +/// the fused token counting (see [`toon_encode_tool_results`]). Passing `None` +/// there skips counting entirely (Anthropic and Bedrock keep their own JS +/// tokenizer); the two variants below cover the tiktoken-family adapters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum))] +pub enum BeforeSource { + /// Count the original `raw_content` (pre-unwrap) as the baseline — Gemini, + /// which tokenizes its serialized response, not the unwrapped inner text. + Raw, + /// Count the `normalized` (post-unwrap) content — every other tiktoken + /// adapter (OpenAI family, Cohere, ZhipuAI, MiniMax). + Normalized, +} + +/// The transformation output for one item. `normalized` is the unwrapped string +/// when unwrapping was requested and matched, else the original `raw_content` +/// (adapters tokenize it for accounting). `encoded` is the TOON encoding, or +/// `None` when the content is not parseable JSON — or when encoding would +/// exceed the anti-amplification output budget (see `encode` module docs); +/// adapters keep the original payload either way. +/// +/// `before_tokens`/`encoded_tokens` are the cl100k counts of the baseline and +/// the compressed candidate, populated only when a [`BeforeSource`] is passed +/// AND `encoded` is `Some` (an unencodable item is never tokenized, matching +/// the adapters). Both `None` otherwise. +/// +/// `use_nullable` makes `None` fields cross the boundary as an explicit JS +/// `null` (typed `string | null` / `number | null`) instead of an omitted key. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "napi", napi_derive::napi(object, use_nullable = true))] +pub struct ToonEncodeResult { + pub normalized: String, + pub encoded: Option, + #[cfg_attr(feature = "napi", napi(js_name = "beforeTokens"))] + pub before_tokens: Option, + #[cfg_attr(feature = "napi", napi(js_name = "encodedTokens"))] + pub encoded_tokens: Option, +} + +/// Items whose raw content exceeds this many bytes are not parsed or encoded +/// at all (`encoded: None`, content kept verbatim, unwrap skipped too). +/// Rationale: multi-MB tool results gain nothing from TOON compression, and +/// the parser DOM expands to roughly 12-24x the input bytes — without the cap +/// a single large item could allocation-abort the host process before any +/// encoder output budget applies. The cap bounds parser memory per item (and +/// so per AsyncTask) to ~cap x expansion. +pub const MAX_ITEM_INPUT_BYTES: usize = 10 * 1024 * 1024; + +/// Transform a batch of tool results. Positional contract: the output has the +/// same length and order as the input. Never panics on any input. +/// +/// Aggregate anti-amplification budget: per-item output budgets have a 16KiB +/// floor, which many small exponent-heavy items could otherwise sum into +/// unbounded retained output (200k tiny items x ~16KiB each). The batch's +/// total produced output is capped at `2 x total input bytes + one floor`; +/// once the running total exceeds it, remaining items are not encoded +/// (`encoded: None`, fail-open like the per-item budget, unwrap still applied). +/// +/// When `count` is `Some`, the cl100k token counts that gate each adapter's +/// keep/reject decision are computed here, in the same off-thread pass, instead +/// of by a synchronous WASM tokenizer on the Node event loop. `None` leaves +/// `before_tokens`/`encoded_tokens` unset (Anthropic/Bedrock count with their +/// own tokenizer). +pub fn toon_encode_tool_results( + items: Vec, + count: Option, +) -> Vec { + let total_input: usize = items + .iter() + .map(|item| item.raw_content.len()) + .fold(0usize, usize::saturating_add); + let batch_budget = total_input + .saturating_mul(2) + .saturating_add(encode::OUTPUT_BUDGET_FLOOR); + let mut produced: usize = 0; + items + .into_iter() + .map(|item| { + let result = encode_item(item, produced <= batch_budget, count); + if let Some(encoded) = &result.encoded { + produced = produced.saturating_add(encoded.len()); + } + result + }) + .collect() +} + +/// The item outcome computed while the parsed DOM still borrows `raw_content`. +enum ItemOutcome { + /// Content was not parseable JSON (or was JSON but exceeded encode limits). + Encoded(Option), + /// The unwrap wrapper matched; the extracted text replaces `raw_content`. + Unwrapped(String), +} + +fn encode_item( + item: ToonEncodeItem, + encode_enabled: bool, + count: Option, +) -> ToonEncodeResult { + // Input size cap: see MAX_ITEM_INPUT_BYTES. Checked before any parse. + if item.raw_content.len() > MAX_ITEM_INPUT_BYTES { + return ToonEncodeResult { + normalized: item.raw_content, + encoded: None, + before_tokens: None, + encoded_tokens: None, + }; + } + // Single DOM parse per item: the unwrap check reuses the parsed value + // instead of parsing the content once to inspect the wrapper and a second + // time to encode. Only a matched wrapper needs the extra inner parse (its + // payload is a JSON string, not a subtree). The DOM borrows from + // `raw_content`, so the outcome is computed before `raw_content` moves. + // When the aggregate batch budget is exhausted (`encode_enabled` false), + // unwrap semantics are preserved but no encoding is produced. + let outcome = match json::parse_json(&item.raw_content) { + None => ItemOutcome::Encoded(None), + Some(value) => { + if item.unwrap { + match take_wrapper_text(value) { + Ok(text) => ItemOutcome::Unwrapped(text), + Err(value) => ItemOutcome::Encoded(encode_value( + &value, + item.raw_content.len(), + encode_enabled, + )), + } + } else { + ItemOutcome::Encoded(encode_value(&value, item.raw_content.len(), encode_enabled)) + } + } + }; + match outcome { + ItemOutcome::Encoded(encoded) => { + // normalized == raw_content on this path, so Raw and Normalized + // select the same string. + let (before_tokens, encoded_tokens) = counts( + count, + &item.raw_content, + &item.raw_content, + encoded.as_deref(), + ); + ToonEncodeResult { + normalized: item.raw_content, + encoded, + before_tokens, + encoded_tokens, + } + } + ItemOutcome::Unwrapped(text) => { + let encoded = if encode_enabled { + json::parse_json(&text) + .and_then(|value| encode::encode_to_toon(&value, text.len()).ok()) + } else { + None + }; + let (before_tokens, encoded_tokens) = + counts(count, &item.raw_content, &text, encoded.as_deref()); + ToonEncodeResult { + normalized: text, + encoded, + before_tokens, + encoded_tokens, + } + } + } +} + +/// cl100k token counts for the keep/reject decision. Computed only for +/// encodable items (`encoded` is `Some`) — every adapter skips tokenizing a +/// result it cannot compress, so counting one here would be wasted work and a +/// meaningless "before". `before` follows the adapter's semantics via +/// [`BeforeSource`]. A `None` from the tokenizer (encoder init failed) is +/// propagated so the binding can fail open rather than report a bogus zero. +fn counts( + count: Option, + raw: &str, + normalized: &str, + encoded: Option<&str>, +) -> (Option, Option) { + match (count, encoded) { + (Some(source), Some(encoded)) => { + let before = match source { + BeforeSource::Raw => raw, + BeforeSource::Normalized => normalized, + }; + ( + tokenize::count_user_tokens(before), + tokenize::count_user_tokens(encoded), + ) + } + _ => (None, None), + } +} + +fn encode_value(value: &JsonValue<'_>, input_len: usize, enabled: bool) -> Option { + if !enabled { + return None; + } + encode::encode_to_toon(value, input_len).ok() +} + +/// Port of `platform/backend/src/routes/proxy/utils/unwrap-tool-content.ts`: +/// if the parsed content is a JSON array whose FIRST element is +/// `{"type": "text", "text": , ...}`, return that text; otherwise give +/// the value back unchanged. First-element-only is deliberate (pinned TS +/// behavior) — extra wrapper elements are dropped from the encoding input. +/// +/// Divergence from JS `JSON.parse` (within the approved migration envelope): +/// `serde_json` rejects escaped lone surrogates (e.g. `"\ud800"`) and +/// out-of-range number literals (e.g. `1e400`, `Infinity` in JS) that JS +/// parses, so wrappers containing them are NOT unwrapped here — the content +/// falls through unchanged and later fails to encode (`encoded: None`), i.e. +/// the original payload is conservatively kept. +fn take_wrapper_text(value: JsonValue<'_>) -> Result> { + let JsonValue::Array(mut elements) = value else { + return Err(value); + }; + let text = match elements.first_mut() { + Some(JsonValue::Object(first)) + if first.get("type").and_then(JsonValue::as_str) == Some("text") => + { + match first.get_mut("text") { + // The wrapper array is discarded on this path, so taking the + // text out of it costs at most one copy (borrowed -> owned). + Some(JsonValue::String(text)) => Some(std::mem::take(text).into_owned()), + _ => None, + } + } + _ => None, + }; + match text { + Some(text) => Ok(text), + None => Err(JsonValue::Array(elements)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn encode_one(raw_content: &str, unwrap: bool) -> ToonEncodeResult { + let results = toon_encode_tool_results( + vec![ToonEncodeItem { + id: "t1".to_string(), + raw_content: raw_content.to_string(), + unwrap, + }], + None, + ); + assert_eq!(results.len(), 1); + results.into_iter().next().expect("one result") + } + + const INNER: &str = r#"{"data":[{"id":1,"v":"a"},{"id":2,"v":"b"}],"ok":true}"#; + const INNER_TOON: &str = "data[2]{id,v}:\n 1,a\n 2,b\nok: true"; + + #[test] + fn unwrap_single_text_wrapper() { + let wrapped = serde_json::to_string(&serde_json::json!([{"type": "text", "text": INNER}])) + .expect("serialize fixture"); + let result = encode_one(&wrapped, true); + assert_eq!(result.normalized, INNER); + assert_eq!(result.encoded.as_deref(), Some(INNER_TOON)); + } + + #[test] + fn unwrap_multi_element_wrapper_uses_first_text_only() { + let wrapped = serde_json::to_string(&serde_json::json!([ + {"type": "text", "text": INNER}, + {"type": "text", "text": r#"{"second":"ignored"}"#}, + ])) + .expect("serialize fixture"); + let result = encode_one(&wrapped, true); + assert_eq!(result.normalized, INNER); + assert_eq!(result.encoded.as_deref(), Some(INNER_TOON)); + } + + #[test] + fn unwrap_first_block_not_text_returns_content_unchanged() { + let wrapped = serde_json::to_string(&serde_json::json!([ + {"type": "image", "url": "http://x"}, + {"type": "text", "text": INNER}, + ])) + .expect("serialize fixture"); + let result = encode_one(&wrapped, true); + // Not unwrapped: the whole wrapper array is what gets encoded. + assert_eq!(result.normalized, wrapped); + assert!(result.encoded.is_some()); + assert_ne!(result.encoded.as_deref(), Some(INNER_TOON)); + } + + #[test] + fn unwrap_text_field_not_a_string_returns_content_unchanged() { + let wrapped = r#"[{"type":"text","text":42}]"#; + let result = encode_one(wrapped, true); + assert_eq!(result.normalized, wrapped); + } + + #[test] + fn unwrap_non_array_json_returns_content_unchanged() { + let result = encode_one(INNER, true); + assert_eq!(result.normalized, INNER); + assert_eq!(result.encoded.as_deref(), Some(INNER_TOON)); + } + + #[test] + fn unwrap_empty_array_returns_content_unchanged() { + let result = encode_one("[]", true); + assert_eq!(result.normalized, "[]"); + assert!(result.encoded.is_some()); + } + + #[test] + fn unwrap_first_element_not_an_object_returns_content_unchanged() { + let raw = r#"["text",{"type":"text","text":"x"}]"#; + let result = encode_one(raw, true); + assert_eq!(result.normalized, raw); + } + + #[test] + fn unwrap_invalid_json_returns_content_unchanged() { + let raw = "not json at all"; + let result = encode_one(raw, true); + assert_eq!(result.normalized, raw); + assert_eq!(result.encoded, None); + } + + #[test] + fn unwrap_false_skips_unwrapping_even_for_wrapper_shape() { + let wrapped = serde_json::to_string(&serde_json::json!([{"type": "text", "text": INNER}])) + .expect("serialize fixture"); + let result = encode_one(&wrapped, false); + assert_eq!(result.normalized, wrapped); + assert_ne!(result.encoded.as_deref(), Some(INNER_TOON)); + } + + #[test] + fn wrapper_with_escaped_lone_surrogate_falls_through_unchanged() { + // JS `JSON.parse` accepts this wrapper and would unwrap it; serde_json + // rejects the escaped lone surrogate, so the content is kept as-is and + // nothing is encoded (approved JS→Rust migration divergence). + let wrapped = r#"[{"type":"text","text":"\ud800"}]"#; + let result = encode_one(wrapped, true); + assert_eq!(result.normalized, wrapped); + assert_eq!(result.encoded, None); + } + + #[test] + fn wrapper_containing_invalid_json_text_yields_no_encoding() { + let wrapped = r#"[{"type":"text","text":"plain prose result"}]"#; + let result = encode_one(wrapped, true); + assert_eq!(result.normalized, "plain prose result"); + assert_eq!(result.encoded, None); + } + + #[test] + fn malformed_json_yields_none_with_raw_normalized() { + for raw in [ + r#"{"a": [1, 2"#, + "Tool run 42 output", + "{'a': 1}", + r#"{"x": NaN}"#, + "", + ] { + let result = encode_one(raw, true); + assert_eq!(result.normalized, raw); + assert_eq!(result.encoded, None, "raw content: {raw:?}"); + } + } + + #[test] + fn non_object_roots_encode() { + for (raw, expected) in [ + ("42", "42"), + (r#""just a string""#, "just a string"), + ("true", "true"), + ("null", "null"), + ("[1,2,3,4,5]", "[5]: 1,2,3,4,5"), + ("{}", ""), + ("[]", "[0]:"), + ] { + let result = encode_one(raw, false); + assert_eq!(result.encoded.as_deref(), Some(expected), "raw: {raw}"); + } + } + + #[test] + fn boundary_numbers_encode_exactly() { + for (raw, expected) in [ + (r#"{"n":9007199254740991}"#, "n: 9007199254740991"), // 2^53 - 1 + (r#"{"n":9007199254740992}"#, "n: 9007199254740992"), // 2^53 + (r#"{"n":9007199254740993}"#, "n: 9007199254740993"), // 2^53 + 1 (JS would coerce) + (r#"{"n":9223372036854775807}"#, "n: 9223372036854775807"), // i64::MAX + (r#"{"n":-9223372036854775808}"#, "n: -9223372036854775808"), // i64::MIN + (r#"{"n":18446744073709551615}"#, "n: 18446744073709551615"), // u64::MAX + (r#"{"x":-0}"#, "x: 0"), + (r#"{"x":1e-7}"#, "x: 0.0000001"), + ] { + let result = encode_one(raw, true); + assert_eq!(result.encoded.as_deref(), Some(expected), "raw: {raw}"); + } + // 1e300 expands to the full decimal literal; pin its shape, not 300 zeros. + let huge = encode_one(r#"{"x":1e300}"#, true) + .encoded + .expect("1e300 encodes"); + assert!(huge.starts_with("x: 1")); + assert_eq!(huge.len(), "x: ".len() + 301); + } + + #[test] + fn aggregate_batch_budget_caps_total_retained_output() { + // Each item is ~301B of exponent-form numbers encoding to ~15.1KB — + // under its own per-item 16KiB floor, so 40 of them would retain + // ~600KB from ~12KB of input without the aggregate cap. Batch budget + // = 2 x 12040 + 16384 = 40464 bytes: items 0-2 encode (45315 bytes + // produced), everything after is skipped. + let raw = format!("[{}]", vec!["1e300"; 50].join(",")); + let items: Vec = (0..40) + .map(|i| ToonEncodeItem { + id: format!("i{i}"), + raw_content: raw.clone(), + unwrap: false, + }) + .collect(); + let total_input = raw.len() * 40; + let batch_budget = 2 * total_input + 16 * 1024; + + let results = toon_encode_tool_results(items, None); + assert_eq!(results.len(), 40); + let encoded_count = results.iter().filter(|r| r.encoded.is_some()).count(); + assert_eq!(encoded_count, 3, "first items encode, tail is skipped"); + assert!(results[0].encoded.is_some()); + assert!(results.last().expect("40 results").encoded.is_none()); + let produced: usize = results + .iter() + .filter_map(|r| r.encoded.as_ref().map(String::len)) + .sum(); + // Bounded: the budget plus at most one crossing item's output. + assert!( + produced <= batch_budget + 16 * 1024, + "retained output {produced} exceeds bound" + ); + assert!(results.iter().all(|r| r.normalized == raw)); + } + + #[test] + fn input_cap_skips_oversized_items_without_parsing() { + // Just over the cap: skipped entirely — no parse, no unwrap, content + // kept verbatim. + let over = format!(r#"["{}"]"#, "a".repeat(MAX_ITEM_INPUT_BYTES)); + assert!(over.len() > MAX_ITEM_INPUT_BYTES); + let result = encode_one(&over, true); + assert_eq!(result.normalized, over); + assert_eq!(result.encoded, None); + + // At the cap boundary: still parsed and encoded normally. + let under = format!(r#"["{}"]"#, "a".repeat(MAX_ITEM_INPUT_BYTES - 4)); + assert_eq!(under.len(), MAX_ITEM_INPUT_BYTES); + let result = encode_one(&under, true); + assert!(result.encoded.is_some()); + } + + #[test] + fn budget_exceeded_yields_none_and_keeps_original() { + // ~24KB of exponent-form numbers would expand ~60x past the 2x output + // budget; the item fails open: `encoded: None`, original payload kept. + // This pins the behavior change for adapters that apply compression + // unconditionally (Bedrock/MiniMax): a pathologically-expanding payload + // is now skipped instead of hugely inflated — closer to the old npm + // path, which emitted compact exponent forms. + let raw = format!("[{}]", vec!["1e300"; 4096].join(",")); + let result = encode_one(&raw, true); + assert_eq!(result.normalized, raw); + assert_eq!(result.encoded, None); + } + + #[test] + fn batch_is_positional_and_same_length() { + let items = vec![ + ToonEncodeItem { + id: "a".to_string(), + raw_content: r#"{"a":1}"#.to_string(), + unwrap: true, + }, + ToonEncodeItem { + id: "a".to_string(), // duplicate id: results are positional + raw_content: "not json".to_string(), + unwrap: true, + }, + ToonEncodeItem { + id: "c".to_string(), + raw_content: r#"{"c":3}"#.to_string(), + unwrap: false, + }, + ]; + let results = toon_encode_tool_results(items, None); + assert_eq!(results.len(), 3); + assert_eq!(results[0].encoded.as_deref(), Some("a: 1")); + assert_eq!(results[1].encoded, None); + assert_eq!(results[2].encoded.as_deref(), Some("c: 3")); + } + + #[test] + fn empty_batch_returns_empty() { + assert!(toon_encode_tool_results(Vec::new(), None).is_empty()); + } + + fn count_one(raw_content: &str, unwrap: bool, count: BeforeSource) -> ToonEncodeResult { + let results = toon_encode_tool_results( + vec![ToonEncodeItem { + id: "t1".to_string(), + raw_content: raw_content.to_string(), + unwrap, + }], + Some(count), + ); + results.into_iter().next().expect("one result") + } + + #[test] + fn no_count_leaves_token_fields_unset() { + let result = encode_one(INNER, true); + assert!(result.encoded.is_some()); + assert_eq!(result.before_tokens, None); + assert_eq!(result.encoded_tokens, None); + } + + #[test] + fn count_populates_both_token_fields_for_encoded_items() { + let result = count_one(INNER, false, BeforeSource::Normalized); + let before = result.before_tokens.expect("before counted"); + let after = result.encoded_tokens.expect("encoded counted"); + assert!(before > 0 && after > 0); + // The whole point: the TOON encoding of a uniform object is smaller. + assert!(after < before, "before={before} after={after}"); + } + + #[test] + fn count_skips_unencodable_items() { + // Not JSON -> encoded None -> no "before" to compare, nothing counted. + let result = count_one("plain prose, not json", true, BeforeSource::Normalized); + assert_eq!(result.encoded, None); + assert_eq!(result.before_tokens, None); + assert_eq!(result.encoded_tokens, None); + } + + #[test] + fn before_source_selects_raw_vs_normalized_when_they_differ() { + // A wrapped payload: normalized (inner INNER) differs from raw (the + // whole wrapper array). Raw counts more tokens than Normalized. + let wrapped = serde_json::to_string(&serde_json::json!([{"type": "text", "text": INNER}])) + .expect("serialize fixture"); + let normalized = count_one(&wrapped, true, BeforeSource::Normalized) + .before_tokens + .expect("normalized before"); + let raw = count_one(&wrapped, true, BeforeSource::Raw) + .before_tokens + .expect("raw before"); + assert!(raw > normalized, "raw={raw} normalized={normalized}"); + } +} diff --git a/platform/archestra-rs/proxy-transform-core/src/tokenize.rs b/platform/archestra-rs/proxy-transform-core/src/tokenize.rs new file mode 100644 index 00000000000..f0a42bd6894 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/src/tokenize.rs @@ -0,0 +1,67 @@ +//! cl100k_base token counting, fused into the encode pass so the LLM proxy's +//! TOON keep/reject decision no longer runs a synchronous WASM tokenizer on the +//! Node event loop (it blocks concurrent requests; the encode already runs +//! off-thread). Byte-identical to the JS path in +//! `backend/src/tokenizers/{tiktoken,base}.ts`. + +use std::sync::LazyLock; + +use tiktoken_rs::CoreBPE; + +/// The JS tokenizer prepends the message role to the content before encoding +/// (`backend/src/tokenizers/base.ts` `getEncodableText`: `${role}${text}`), and +/// all five tiktoken-family TOON adapters count under role `"user"`. Matching +/// that prefix is part of the parity contract enforced by the Node differential +/// test; a divergence here silently shifts recorded token counts. +const ROLE_PREFIX: &str = "user"; + +/// Process-wide cl100k_base encoder. Construction parses a vendored rank table; +/// a failure yields `None` and counting is reported as unavailable rather than +/// panicking — the core promises never to panic (the adapters then fail open to +/// the `addon_unavailable` skip reason, exactly like a missing addon). +static CL100K: LazyLock> = LazyLock::new(|| tiktoken_rs::cl100k_base().ok()); + +/// Count cl100k tokens of `role + text`, matching JS +/// `countTokens([{ role: "user", content: text }])`. `None` only when the +/// encoder failed to initialize. Uses ordinary encoding: special-token literals +/// (e.g. `<|endoftext|>`) in tool results are counted as plain text instead of +/// raising, matching the ordinary-encoding baseline the JS tokenizer now uses. +pub(crate) fn count_user_tokens(text: &str) -> Option { + let bpe = CL100K.as_ref()?; + let mut prefixed = String::with_capacity(ROLE_PREFIX.len() + text.len()); + prefixed.push_str(ROLE_PREFIX); + prefixed.push_str(text); + Some(bpe.encode_ordinary(&prefixed).len() as u32) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_are_available_and_positive() { + assert!(count_user_tokens("hello world").is_some_and(|n| n > 0)); + } + + #[test] + fn empty_text_counts_the_role_prefix_only() { + // "user" alone is one cl100k token; the count is deterministic and the + // role prefix is always present. + assert_eq!(count_user_tokens(""), count_user_tokens("")); + assert!(count_user_tokens("").is_some_and(|n| n >= 1)); + } + + #[test] + fn special_token_literals_do_not_raise() { + // Ordinary encoding: the reserved marker is counted as text, not an + // error (the JS baseline encodes ordinally too). + assert!(count_user_tokens("<|endoftext|> in a tool result").is_some_and(|n| n > 0)); + } + + #[test] + fn longer_text_counts_more() { + let short = count_user_tokens("a").expect("bpe"); + let long = count_user_tokens(&"lorem ipsum dolor sit amet ".repeat(20)).expect("bpe"); + assert!(long > short); + } +} diff --git a/platform/archestra-rs/proxy-transform-core/tests/fixtures/gen-corpus.mts b/platform/archestra-rs/proxy-transform-core/tests/fixtures/gen-corpus.mts new file mode 100644 index 00000000000..80e34bdcbf5 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/tests/fixtures/gen-corpus.mts @@ -0,0 +1,191 @@ +// Golden-corpus INPUT generator (deterministic). Writes the `name`/`rawContent`/ +// `unwrap` fields of golden-corpus.json; the expected outputs are then filled in +// by the Rust side: +// 1. from platform/backend: +// pnpm exec tsx ../archestra-rs/proxy-transform-core/tests/fixtures/gen-corpus.mts +// 2. from platform/archestra-rs: +// UPDATE_TOON_GOLDENS=1 cargo test -p proxy_transform_core --test golden_corpus +// +// Raw contents for boundary numbers are hand-written strings so exact JSON +// literals survive (JS values would coerce before serialization). The bench +// items reuse the T0 benchmark corpus builder for realistic kernel shapes. +import { writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildBatch } from "../../../../backend/src/routes/proxy/__bench__/corpus"; + +interface CorpusItem { + name: string; + rawContent: string; + unwrap: boolean; +} + +const TOOL_RESULT_DATA = { + files: [ + { name: "README.md", size: 1024, type: "file" }, + { name: "src", size: 4096, type: "directory" }, + { name: "package.json", size: 512, type: "file" }, + { name: "tsconfig.json", size: 256, type: "file" }, + { name: "node_modules", size: 102400, type: "directory" }, + ], + totalCount: 5, + directory: ".", +}; + +const items: CorpusItem[] = []; +const add = (name: string, rawContent: string, unwrap = true) => + items.push({ name, rawContent, unwrap }); + +// --- ordering --- +add("order-integer-like-keys", '{"2":"two","10":"ten","a":1,"1":"one","b":{"20":true,"3":false,"x":null}}'); +add("order-array-of-objs-mixed-keys", '[{"10":1,"2":2,"z":3},{"10":4,"2":5,"z":6}]'); + +// --- boundary numbers --- +add("num-2p53-minus1", '{"n":9007199254740991}'); +add("num-2p53", '{"n":9007199254740992}'); +add("num-2p53-plus1", '{"n":9007199254740993}'); +add("num-neg-2p53-plus1", '{"n":-9007199254740993}'); +add("num-i64-max", '{"n":9223372036854775807}'); +add("num-i64-max-plus1", '{"n":9223372036854775808}'); +add("num-i64-min", '{"n":-9223372036854775808}'); +add("num-u64-max", '{"n":18446744073709551615}'); +add("num-u64-max-plus1", '{"n":18446744073709551616}'); +add("num-1e21-exp", '{"x":1e21}'); +add("num-1e21-expanded", '{"x":1000000000000000000000}'); +add("num-1e300", '{"x":1e300}'); +add("num-neg-1e21", '{"x":-1e21}'); +add("num-1e-7", '{"x":1e-7}'); +add("num-1e-300", '{"x":1e-300}'); +add("num-neg-zero", '{"x":-0}'); +add("num-neg-zero-float", '{"x":-0.0}'); +add("num-float-precision", '{"x":0.1,"y":1.005,"z":123.456e2}'); +add("num-large-neg", '{"x":-1.7976931348623157e308}'); + +// --- escaping / unicode --- +add("esc-quotes-commas-colons", '{"a":"he said \\"hi, there\\": ok","b":"comma,separated","c":"colon: value","d":"[bracket] {brace}"}'); +add("esc-control-chars", '{"a":"line1\\nline2\\ttabbed","b":"back\\\\slash","c":"nul\\u0000end","d":"\\u001b[31mred"}'); +add("esc-toon-specials", '{"a":" leading spaces","b":"trailing ","c":"#comment-ish","d":"- dash start","e":"|pipe|","f":"true","g":"123","h":"null","i":""}'); +add("unicode-mixed", '{"emoji":"🎉🚀 done","cjk":"日本語テスト","combining":"éé","rtl":"مرحبا","surrogate":"𝄞 music"}'); + +// --- nesting / roots --- +add("nested-deep", '{"a":{"b":{"c":{"d":{"e":[1,{"f":[true,null,{"g":"deep"}]}]}}}}}'); +add("nested-mixed-arrays", '[[1,2],[3,[4,5]],{"k":[{"a":1},{"a":2}]}]'); +add("root-string", '"just a string"'); +add("root-number", "42"); +add("root-float", "3.14"); +add("root-true", "true"); +add("root-null", "null"); +add("root-array-scalars", "[1,2,3,4,5]"); +add("root-array-strings", '["a","b","c with, comma"]'); +add("empty-object", "{}"); +add("empty-array", "[]"); +add("array-of-empty-objects", "[{},{},{}]"); // encode-only golden: toon-format issue #74 (decoder rejects `[N]{}:`) +add("array-with-one-empty-object", '[{"a":1},{}]'); + +// --- malformed (must be skipped, encoded=null) --- +add("malformed-truncated", '{"a": [1, 2'); +add("malformed-prose", "Tool run 42 output: alpha bravo charlie"); +add("malformed-single-quotes", "{'a': 1}"); +add("malformed-nan", '{"x": NaN}'); +add("malformed-infinity", '{"x": Infinity}'); +add("malformed-empty", ""); + +// --- wrappers --- +const inner = JSON.stringify({ data: [{ id: 1, v: "a" }, { id: 2, v: "b" }], ok: true }); +add("wrapped-single-text", JSON.stringify([{ type: "text", text: inner }])); +add("wrapped-multi-text", JSON.stringify([ + { type: "text", text: inner }, + { type: "text", text: '{"second":"element ignored by unwrap"}' }, +])); +add("wrapped-first-not-text", JSON.stringify([ + { type: "image", url: "http://x" }, + { type: "text", text: inner }, +])); +add("wrapped-text-not-json", JSON.stringify([{ type: "text", text: "plain prose result" }])); +add("wrapped-but-unwrap-false", JSON.stringify([{ type: "text", text: inner }]), false); + +// --- realistic provider payloads --- +add("provider-matrix-tool-result", JSON.stringify(TOOL_RESULT_DATA)); +add("bedrock-json-branch", JSON.stringify(TOOL_RESULT_DATA), false); +add("realistic-github-issues", JSON.stringify({ + items: Array.from({ length: 12 }, (_, i) => ({ + number: 100 + i, + title: `Issue title number ${i}: something broke, badly`, + state: i % 3 === 0 ? "closed" : "open", + user: { login: `user${i}`, id: 1000 + i }, + labels: [`bug`, `p${i % 3}`], + comments: i * 2, + created_at: `2026-06-${String(1 + i).padStart(2, "0")}T10:00:00Z`, + })), + total_count: 12, +})); +add("realistic-db-rows", JSON.stringify( + Array.from({ length: 20 }, (_, i) => ({ + id: i, + email: `person${i}@example.com`, + balance: Math.round((i * 137.13 % 1000) * 100) / 100, + active: i % 2 === 0, + region: ["us-east", "eu-west", "ap-south"][i % 3], + })), +)); + +// --- bench-corpus derived (realistic kernel shapes, deterministic seed) --- +for (const [i, benchItem] of buildBatch({ name: "1KB", payloadBytes: 1 << 10, count: 10 }, 42).entries()) { + items.push({ name: `bench-1kb-${i}`, rawContent: benchItem.rawContent, unwrap: benchItem.unwrap }); +} +for (const [i, benchItem] of buildBatch({ name: "10KB", payloadBytes: 10 << 10, count: 3 }, 7).entries()) { + items.push({ name: `bench-10kb-${i}`, rawContent: benchItem.rawContent, unwrap: benchItem.unwrap }); +} + +// --- near-boundary family: engineered so TOON savings hover around zero. +// Heterogeneous keys + comma-laden strings kill the tabular win; sweep sizes +// so some land within ±2 tokens of the original. +for (let k = 1; k <= 10; k++) { + const obj: Record = {}; + for (let j = 0; j < k; j++) { + obj[`key_${j}`] = j % 2 === 0 ? `val, with comma ${j}` : j * 1.5; + } + add(`boundary-obj-${k}`, JSON.stringify(obj)); +} +for (let k = 1; k <= 6; k++) { + const arr = Array.from({ length: k }, (_, j) => ({ + [`f${j}`]: `x,y ${j}`, + n: j, + })); + add(`boundary-hetero-arr-${k}`, JSON.stringify(arr)); +} + +// Hyphenated values are quoted by the v3 encoder ('-' is structural), so the +// token savings sweep crosses zero around these sizes — near-boundary coverage +// for keep/reject decisions. +for (let k = 1; k <= 8; k++) { + const arr = Array.from({ length: k }, (_, j) => ({ + sku: `AB-${100 + j}`, + zone: `us-east-${j}`, + n: j, + })); + add(`boundary-hyphen-arr-${k}`, JSON.stringify(arr)); +} + +// Finer sweep around the keep/reject crossing: 1-2 rows, mixing bare fields +// (savings source) and hyphenated fields (quoting penalty), varying counts so +// savings land at -1, 0, +1, +2 tokens. +for (let bare = 0; bare <= 4; bare++) { + for (let hyph = 1; hyph <= 3; hyph++) { + for (const rows of [1, 2]) { + const arr = Array.from({ length: rows }, (_, r) => { + const row: Record = {}; + for (let j = 0; j < bare; j++) row[`b${j}`] = `plain${r}${j}`; + for (let j = 0; j < hyph; j++) row[`h${j}`] = `us-east-${r}${j}`; + return row; + }); + add(`fine-r${rows}-b${bare}-h${hyph}`, JSON.stringify(arr)); + } + } +} + +const out = + process.argv[2] ?? + join(dirname(fileURLToPath(import.meta.url)), "golden-corpus.json"); +writeFileSync(out, `${JSON.stringify(items, null, 2)}\n`); +console.log(`wrote ${items.length} corpus items to ${out}`); diff --git a/platform/archestra-rs/proxy-transform-core/tests/fixtures/golden-corpus.json b/platform/archestra-rs/proxy-transform-core/tests/fixtures/golden-corpus.json new file mode 100644 index 00000000000..3d82c2cb5b1 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/tests/fixtures/golden-corpus.json @@ -0,0 +1,1082 @@ +[ + { + "name": "order-integer-like-keys", + "rawContent": "{\"2\":\"two\",\"10\":\"ten\",\"a\":1,\"1\":\"one\",\"b\":{\"20\":true,\"3\":false,\"x\":null}}", + "unwrap": true, + "expected": { + "normalized": "{\"2\":\"two\",\"10\":\"ten\",\"a\":1,\"1\":\"one\",\"b\":{\"20\":true,\"3\":false,\"x\":null}}", + "encoded": "\"2\": two\n\"10\": ten\na: 1\n\"1\": one\nb:\n \"20\": true\n \"3\": false\n x: null" + } + }, + { + "name": "order-array-of-objs-mixed-keys", + "rawContent": "[{\"10\":1,\"2\":2,\"z\":3},{\"10\":4,\"2\":5,\"z\":6}]", + "unwrap": true, + "expected": { + "normalized": "[{\"10\":1,\"2\":2,\"z\":3},{\"10\":4,\"2\":5,\"z\":6}]", + "encoded": "[2]{\"10\",\"2\",z}:\n 1,2,3\n 4,5,6" + } + }, + { + "name": "num-2p53-minus1", + "rawContent": "{\"n\":9007199254740991}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":9007199254740991}", + "encoded": "n: 9007199254740991" + } + }, + { + "name": "num-2p53", + "rawContent": "{\"n\":9007199254740992}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":9007199254740992}", + "encoded": "n: 9007199254740992" + } + }, + { + "name": "num-2p53-plus1", + "rawContent": "{\"n\":9007199254740993}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":9007199254740993}", + "encoded": "n: 9007199254740993" + } + }, + { + "name": "num-neg-2p53-plus1", + "rawContent": "{\"n\":-9007199254740993}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":-9007199254740993}", + "encoded": "n: -9007199254740993" + } + }, + { + "name": "num-i64-max", + "rawContent": "{\"n\":9223372036854775807}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":9223372036854775807}", + "encoded": "n: 9223372036854775807" + } + }, + { + "name": "num-i64-max-plus1", + "rawContent": "{\"n\":9223372036854775808}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":9223372036854775808}", + "encoded": "n: 9223372036854775808" + } + }, + { + "name": "num-i64-min", + "rawContent": "{\"n\":-9223372036854775808}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":-9223372036854775808}", + "encoded": "n: -9223372036854775808" + } + }, + { + "name": "num-u64-max", + "rawContent": "{\"n\":18446744073709551615}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":18446744073709551615}", + "encoded": "n: 18446744073709551615" + } + }, + { + "name": "num-u64-max-plus1", + "rawContent": "{\"n\":18446744073709551616}", + "unwrap": true, + "expected": { + "normalized": "{\"n\":18446744073709551616}", + "encoded": "n: 18446744073709551615" + } + }, + { + "name": "num-1e21-exp", + "rawContent": "{\"x\":1e21}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":1e21}", + "encoded": "x: 1000000000000000000000" + } + }, + { + "name": "num-1e21-expanded", + "rawContent": "{\"x\":1000000000000000000000}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":1000000000000000000000}", + "encoded": "x: 1000000000000000000000" + } + }, + { + "name": "num-1e300", + "rawContent": "{\"x\":1e300}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":1e300}", + "encoded": "x: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "name": "num-neg-1e21", + "rawContent": "{\"x\":-1e21}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":-1e21}", + "encoded": "x: -1000000000000000000000" + } + }, + { + "name": "num-1e-7", + "rawContent": "{\"x\":1e-7}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":1e-7}", + "encoded": "x: 0.0000001" + } + }, + { + "name": "num-1e-300", + "rawContent": "{\"x\":1e-300}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":1e-300}", + "encoded": "x: 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" + } + }, + { + "name": "num-neg-zero", + "rawContent": "{\"x\":-0}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":-0}", + "encoded": "x: 0" + } + }, + { + "name": "num-neg-zero-float", + "rawContent": "{\"x\":-0.0}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":-0.0}", + "encoded": "x: 0" + } + }, + { + "name": "num-float-precision", + "rawContent": "{\"x\":0.1,\"y\":1.005,\"z\":123.456e2}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":0.1,\"y\":1.005,\"z\":123.456e2}", + "encoded": "x: 0.1\ny: 1.005\nz: 12345.6" + } + }, + { + "name": "num-large-neg", + "rawContent": "{\"x\":-1.7976931348623157e308}", + "unwrap": true, + "expected": { + "normalized": "{\"x\":-1.7976931348623157e308}", + "encoded": "x: -179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "name": "esc-quotes-commas-colons", + "rawContent": "{\"a\":\"he said \\\"hi, there\\\": ok\",\"b\":\"comma,separated\",\"c\":\"colon: value\",\"d\":\"[bracket] {brace}\"}", + "unwrap": true, + "expected": { + "normalized": "{\"a\":\"he said \\\"hi, there\\\": ok\",\"b\":\"comma,separated\",\"c\":\"colon: value\",\"d\":\"[bracket] {brace}\"}", + "encoded": "a: \"he said \\\"hi, there\\\": ok\"\nb: \"comma,separated\"\nc: \"colon: value\"\nd: \"[bracket] {brace}\"" + } + }, + { + "name": "esc-control-chars", + "rawContent": "{\"a\":\"line1\\nline2\\ttabbed\",\"b\":\"back\\\\slash\",\"c\":\"nul\\u0000end\",\"d\":\"\\u001b[31mred\"}", + "unwrap": true, + "expected": { + "normalized": "{\"a\":\"line1\\nline2\\ttabbed\",\"b\":\"back\\\\slash\",\"c\":\"nul\\u0000end\",\"d\":\"\\u001b[31mred\"}", + "encoded": "a: \"line1\\nline2\\ttabbed\"\nb: \"back\\\\slash\"\nc: nul\u0000end\nd: \"\u001b[31mred\"" + } + }, + { + "name": "esc-toon-specials", + "rawContent": "{\"a\":\" leading spaces\",\"b\":\"trailing \",\"c\":\"#comment-ish\",\"d\":\"- dash start\",\"e\":\"|pipe|\",\"f\":\"true\",\"g\":\"123\",\"h\":\"null\",\"i\":\"\"}", + "unwrap": true, + "expected": { + "normalized": "{\"a\":\" leading spaces\",\"b\":\"trailing \",\"c\":\"#comment-ish\",\"d\":\"- dash start\",\"e\":\"|pipe|\",\"f\":\"true\",\"g\":\"123\",\"h\":\"null\",\"i\":\"\"}", + "encoded": "a: \" leading spaces\"\nb: \"trailing \"\nc: \"#comment-ish\"\nd: \"- dash start\"\ne: |pipe|\nf: \"true\"\ng: \"123\"\nh: \"null\"\ni: \"\"" + } + }, + { + "name": "unicode-mixed", + "rawContent": "{\"emoji\":\"🎉🚀 done\",\"cjk\":\"日本語テスト\",\"combining\":\"éé\",\"rtl\":\"مرحبا\",\"surrogate\":\"𝄞 music\"}", + "unwrap": true, + "expected": { + "normalized": "{\"emoji\":\"🎉🚀 done\",\"cjk\":\"日本語テスト\",\"combining\":\"éé\",\"rtl\":\"مرحبا\",\"surrogate\":\"𝄞 music\"}", + "encoded": "emoji: 🎉🚀 done\ncjk: 日本語テスト\ncombining: éé\nrtl: مرحبا\nsurrogate: 𝄞 music" + } + }, + { + "name": "nested-deep", + "rawContent": "{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":[1,{\"f\":[true,null,{\"g\":\"deep\"}]}]}}}}}", + "unwrap": true, + "expected": { + "normalized": "{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":[1,{\"f\":[true,null,{\"g\":\"deep\"}]}]}}}}}", + "encoded": "a:\n b:\n c:\n d:\n e[2]:\n - 1\n - f[3]:\n - true\n - null\n - g: deep" + } + }, + { + "name": "nested-mixed-arrays", + "rawContent": "[[1,2],[3,[4,5]],{\"k\":[{\"a\":1},{\"a\":2}]}]", + "unwrap": true, + "expected": { + "normalized": "[[1,2],[3,[4,5]],{\"k\":[{\"a\":1},{\"a\":2}]}]", + "encoded": "[3]:\n - [2]: 1,2\n - [2]:\n - 3\n - [2]: 4,5\n - k[2]{a}:\n 1\n 2" + } + }, + { + "name": "root-string", + "rawContent": "\"just a string\"", + "unwrap": true, + "expected": { + "normalized": "\"just a string\"", + "encoded": "just a string" + } + }, + { + "name": "root-number", + "rawContent": "42", + "unwrap": true, + "expected": { + "normalized": "42", + "encoded": "42" + } + }, + { + "name": "root-float", + "rawContent": "3.14", + "unwrap": true, + "expected": { + "normalized": "3.14", + "encoded": "3.14" + } + }, + { + "name": "root-true", + "rawContent": "true", + "unwrap": true, + "expected": { + "normalized": "true", + "encoded": "true" + } + }, + { + "name": "root-null", + "rawContent": "null", + "unwrap": true, + "expected": { + "normalized": "null", + "encoded": "null" + } + }, + { + "name": "root-array-scalars", + "rawContent": "[1,2,3,4,5]", + "unwrap": true, + "expected": { + "normalized": "[1,2,3,4,5]", + "encoded": "[5]: 1,2,3,4,5" + } + }, + { + "name": "root-array-strings", + "rawContent": "[\"a\",\"b\",\"c with, comma\"]", + "unwrap": true, + "expected": { + "normalized": "[\"a\",\"b\",\"c with, comma\"]", + "encoded": "[3]: a,b,\"c with, comma\"" + } + }, + { + "name": "empty-object", + "rawContent": "{}", + "unwrap": true, + "expected": { + "normalized": "{}", + "encoded": "" + } + }, + { + "name": "empty-array", + "rawContent": "[]", + "unwrap": true, + "expected": { + "normalized": "[]", + "encoded": "[0]:" + } + }, + { + "name": "array-of-empty-objects", + "rawContent": "[{},{},{}]", + "unwrap": true, + "expected": { + "normalized": "[{},{},{}]", + "encoded": "[3]{}:\n \n \n " + } + }, + { + "name": "array-with-one-empty-object", + "rawContent": "[{\"a\":1},{}]", + "unwrap": true, + "expected": { + "normalized": "[{\"a\":1},{}]", + "encoded": "[2]:\n - a: 1\n -" + } + }, + { + "name": "malformed-truncated", + "rawContent": "{\"a\": [1, 2", + "unwrap": true, + "expected": { + "normalized": "{\"a\": [1, 2", + "encoded": null + } + }, + { + "name": "malformed-prose", + "rawContent": "Tool run 42 output: alpha bravo charlie", + "unwrap": true, + "expected": { + "normalized": "Tool run 42 output: alpha bravo charlie", + "encoded": null + } + }, + { + "name": "malformed-single-quotes", + "rawContent": "{'a': 1}", + "unwrap": true, + "expected": { + "normalized": "{'a': 1}", + "encoded": null + } + }, + { + "name": "malformed-nan", + "rawContent": "{\"x\": NaN}", + "unwrap": true, + "expected": { + "normalized": "{\"x\": NaN}", + "encoded": null + } + }, + { + "name": "malformed-infinity", + "rawContent": "{\"x\": Infinity}", + "unwrap": true, + "expected": { + "normalized": "{\"x\": Infinity}", + "encoded": null + } + }, + { + "name": "malformed-empty", + "rawContent": "", + "unwrap": true, + "expected": { + "normalized": "", + "encoded": null + } + }, + { + "name": "wrapped-single-text", + "rawContent": "[{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"}]", + "unwrap": true, + "expected": { + "normalized": "{\"data\":[{\"id\":1,\"v\":\"a\"},{\"id\":2,\"v\":\"b\"}],\"ok\":true}", + "encoded": "data[2]{id,v}:\n 1,a\n 2,b\nok: true" + } + }, + { + "name": "wrapped-multi-text", + "rawContent": "[{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"},{\"type\":\"text\",\"text\":\"{\\\"second\\\":\\\"element ignored by unwrap\\\"}\"}]", + "unwrap": true, + "expected": { + "normalized": "{\"data\":[{\"id\":1,\"v\":\"a\"},{\"id\":2,\"v\":\"b\"}],\"ok\":true}", + "encoded": "data[2]{id,v}:\n 1,a\n 2,b\nok: true" + } + }, + { + "name": "wrapped-first-not-text", + "rawContent": "[{\"type\":\"image\",\"url\":\"http://x\"},{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"type\":\"image\",\"url\":\"http://x\"},{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"}]", + "encoded": "[2]:\n - type: image\n url: \"http://x\"\n - type: text\n text: \"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"" + } + }, + { + "name": "wrapped-text-not-json", + "rawContent": "[{\"type\":\"text\",\"text\":\"plain prose result\"}]", + "unwrap": true, + "expected": { + "normalized": "plain prose result", + "encoded": null + } + }, + { + "name": "wrapped-but-unwrap-false", + "rawContent": "[{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"}]", + "unwrap": false, + "expected": { + "normalized": "[{\"type\":\"text\",\"text\":\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"}]", + "encoded": "[1]{type,text}:\n text,\"{\\\"data\\\":[{\\\"id\\\":1,\\\"v\\\":\\\"a\\\"},{\\\"id\\\":2,\\\"v\\\":\\\"b\\\"}],\\\"ok\\\":true}\"" + } + }, + { + "name": "provider-matrix-tool-result", + "rawContent": "{\"files\":[{\"name\":\"README.md\",\"size\":1024,\"type\":\"file\"},{\"name\":\"src\",\"size\":4096,\"type\":\"directory\"},{\"name\":\"package.json\",\"size\":512,\"type\":\"file\"},{\"name\":\"tsconfig.json\",\"size\":256,\"type\":\"file\"},{\"name\":\"node_modules\",\"size\":102400,\"type\":\"directory\"}],\"totalCount\":5,\"directory\":\".\"}", + "unwrap": true, + "expected": { + "normalized": "{\"files\":[{\"name\":\"README.md\",\"size\":1024,\"type\":\"file\"},{\"name\":\"src\",\"size\":4096,\"type\":\"directory\"},{\"name\":\"package.json\",\"size\":512,\"type\":\"file\"},{\"name\":\"tsconfig.json\",\"size\":256,\"type\":\"file\"},{\"name\":\"node_modules\",\"size\":102400,\"type\":\"directory\"}],\"totalCount\":5,\"directory\":\".\"}", + "encoded": "files[5]{name,size,type}:\n README.md,1024,file\n src,4096,directory\n package.json,512,file\n tsconfig.json,256,file\n node_modules,102400,directory\ntotalCount: 5\ndirectory: ." + } + }, + { + "name": "bedrock-json-branch", + "rawContent": "{\"files\":[{\"name\":\"README.md\",\"size\":1024,\"type\":\"file\"},{\"name\":\"src\",\"size\":4096,\"type\":\"directory\"},{\"name\":\"package.json\",\"size\":512,\"type\":\"file\"},{\"name\":\"tsconfig.json\",\"size\":256,\"type\":\"file\"},{\"name\":\"node_modules\",\"size\":102400,\"type\":\"directory\"}],\"totalCount\":5,\"directory\":\".\"}", + "unwrap": false, + "expected": { + "normalized": "{\"files\":[{\"name\":\"README.md\",\"size\":1024,\"type\":\"file\"},{\"name\":\"src\",\"size\":4096,\"type\":\"directory\"},{\"name\":\"package.json\",\"size\":512,\"type\":\"file\"},{\"name\":\"tsconfig.json\",\"size\":256,\"type\":\"file\"},{\"name\":\"node_modules\",\"size\":102400,\"type\":\"directory\"}],\"totalCount\":5,\"directory\":\".\"}", + "encoded": "files[5]{name,size,type}:\n README.md,1024,file\n src,4096,directory\n package.json,512,file\n tsconfig.json,256,file\n node_modules,102400,directory\ntotalCount: 5\ndirectory: ." + } + }, + { + "name": "realistic-github-issues", + "rawContent": "{\"items\":[{\"number\":100,\"title\":\"Issue title number 0: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user0\",\"id\":1000},\"labels\":[\"bug\",\"p0\"],\"comments\":0,\"created_at\":\"2026-06-01T10:00:00Z\"},{\"number\":101,\"title\":\"Issue title number 1: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user1\",\"id\":1001},\"labels\":[\"bug\",\"p1\"],\"comments\":2,\"created_at\":\"2026-06-02T10:00:00Z\"},{\"number\":102,\"title\":\"Issue title number 2: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user2\",\"id\":1002},\"labels\":[\"bug\",\"p2\"],\"comments\":4,\"created_at\":\"2026-06-03T10:00:00Z\"},{\"number\":103,\"title\":\"Issue title number 3: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user3\",\"id\":1003},\"labels\":[\"bug\",\"p0\"],\"comments\":6,\"created_at\":\"2026-06-04T10:00:00Z\"},{\"number\":104,\"title\":\"Issue title number 4: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user4\",\"id\":1004},\"labels\":[\"bug\",\"p1\"],\"comments\":8,\"created_at\":\"2026-06-05T10:00:00Z\"},{\"number\":105,\"title\":\"Issue title number 5: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user5\",\"id\":1005},\"labels\":[\"bug\",\"p2\"],\"comments\":10,\"created_at\":\"2026-06-06T10:00:00Z\"},{\"number\":106,\"title\":\"Issue title number 6: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user6\",\"id\":1006},\"labels\":[\"bug\",\"p0\"],\"comments\":12,\"created_at\":\"2026-06-07T10:00:00Z\"},{\"number\":107,\"title\":\"Issue title number 7: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user7\",\"id\":1007},\"labels\":[\"bug\",\"p1\"],\"comments\":14,\"created_at\":\"2026-06-08T10:00:00Z\"},{\"number\":108,\"title\":\"Issue title number 8: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user8\",\"id\":1008},\"labels\":[\"bug\",\"p2\"],\"comments\":16,\"created_at\":\"2026-06-09T10:00:00Z\"},{\"number\":109,\"title\":\"Issue title number 9: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user9\",\"id\":1009},\"labels\":[\"bug\",\"p0\"],\"comments\":18,\"created_at\":\"2026-06-10T10:00:00Z\"},{\"number\":110,\"title\":\"Issue title number 10: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user10\",\"id\":1010},\"labels\":[\"bug\",\"p1\"],\"comments\":20,\"created_at\":\"2026-06-11T10:00:00Z\"},{\"number\":111,\"title\":\"Issue title number 11: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user11\",\"id\":1011},\"labels\":[\"bug\",\"p2\"],\"comments\":22,\"created_at\":\"2026-06-12T10:00:00Z\"}],\"total_count\":12}", + "unwrap": true, + "expected": { + "normalized": "{\"items\":[{\"number\":100,\"title\":\"Issue title number 0: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user0\",\"id\":1000},\"labels\":[\"bug\",\"p0\"],\"comments\":0,\"created_at\":\"2026-06-01T10:00:00Z\"},{\"number\":101,\"title\":\"Issue title number 1: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user1\",\"id\":1001},\"labels\":[\"bug\",\"p1\"],\"comments\":2,\"created_at\":\"2026-06-02T10:00:00Z\"},{\"number\":102,\"title\":\"Issue title number 2: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user2\",\"id\":1002},\"labels\":[\"bug\",\"p2\"],\"comments\":4,\"created_at\":\"2026-06-03T10:00:00Z\"},{\"number\":103,\"title\":\"Issue title number 3: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user3\",\"id\":1003},\"labels\":[\"bug\",\"p0\"],\"comments\":6,\"created_at\":\"2026-06-04T10:00:00Z\"},{\"number\":104,\"title\":\"Issue title number 4: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user4\",\"id\":1004},\"labels\":[\"bug\",\"p1\"],\"comments\":8,\"created_at\":\"2026-06-05T10:00:00Z\"},{\"number\":105,\"title\":\"Issue title number 5: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user5\",\"id\":1005},\"labels\":[\"bug\",\"p2\"],\"comments\":10,\"created_at\":\"2026-06-06T10:00:00Z\"},{\"number\":106,\"title\":\"Issue title number 6: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user6\",\"id\":1006},\"labels\":[\"bug\",\"p0\"],\"comments\":12,\"created_at\":\"2026-06-07T10:00:00Z\"},{\"number\":107,\"title\":\"Issue title number 7: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user7\",\"id\":1007},\"labels\":[\"bug\",\"p1\"],\"comments\":14,\"created_at\":\"2026-06-08T10:00:00Z\"},{\"number\":108,\"title\":\"Issue title number 8: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user8\",\"id\":1008},\"labels\":[\"bug\",\"p2\"],\"comments\":16,\"created_at\":\"2026-06-09T10:00:00Z\"},{\"number\":109,\"title\":\"Issue title number 9: something broke, badly\",\"state\":\"closed\",\"user\":{\"login\":\"user9\",\"id\":1009},\"labels\":[\"bug\",\"p0\"],\"comments\":18,\"created_at\":\"2026-06-10T10:00:00Z\"},{\"number\":110,\"title\":\"Issue title number 10: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user10\",\"id\":1010},\"labels\":[\"bug\",\"p1\"],\"comments\":20,\"created_at\":\"2026-06-11T10:00:00Z\"},{\"number\":111,\"title\":\"Issue title number 11: something broke, badly\",\"state\":\"open\",\"user\":{\"login\":\"user11\",\"id\":1011},\"labels\":[\"bug\",\"p2\"],\"comments\":22,\"created_at\":\"2026-06-12T10:00:00Z\"}],\"total_count\":12}", + "encoded": "items[12]:\n - number: 100\n title: \"Issue title number 0: something broke, badly\"\n state: closed\n user:\n login: user0\n id: 1000\n labels[2]: bug,p0\n comments: 0\n created_at: \"2026-06-01T10:00:00Z\"\n - number: 101\n title: \"Issue title number 1: something broke, badly\"\n state: open\n user:\n login: user1\n id: 1001\n labels[2]: bug,p1\n comments: 2\n created_at: \"2026-06-02T10:00:00Z\"\n - number: 102\n title: \"Issue title number 2: something broke, badly\"\n state: open\n user:\n login: user2\n id: 1002\n labels[2]: bug,p2\n comments: 4\n created_at: \"2026-06-03T10:00:00Z\"\n - number: 103\n title: \"Issue title number 3: something broke, badly\"\n state: closed\n user:\n login: user3\n id: 1003\n labels[2]: bug,p0\n comments: 6\n created_at: \"2026-06-04T10:00:00Z\"\n - number: 104\n title: \"Issue title number 4: something broke, badly\"\n state: open\n user:\n login: user4\n id: 1004\n labels[2]: bug,p1\n comments: 8\n created_at: \"2026-06-05T10:00:00Z\"\n - number: 105\n title: \"Issue title number 5: something broke, badly\"\n state: open\n user:\n login: user5\n id: 1005\n labels[2]: bug,p2\n comments: 10\n created_at: \"2026-06-06T10:00:00Z\"\n - number: 106\n title: \"Issue title number 6: something broke, badly\"\n state: closed\n user:\n login: user6\n id: 1006\n labels[2]: bug,p0\n comments: 12\n created_at: \"2026-06-07T10:00:00Z\"\n - number: 107\n title: \"Issue title number 7: something broke, badly\"\n state: open\n user:\n login: user7\n id: 1007\n labels[2]: bug,p1\n comments: 14\n created_at: \"2026-06-08T10:00:00Z\"\n - number: 108\n title: \"Issue title number 8: something broke, badly\"\n state: open\n user:\n login: user8\n id: 1008\n labels[2]: bug,p2\n comments: 16\n created_at: \"2026-06-09T10:00:00Z\"\n - number: 109\n title: \"Issue title number 9: something broke, badly\"\n state: closed\n user:\n login: user9\n id: 1009\n labels[2]: bug,p0\n comments: 18\n created_at: \"2026-06-10T10:00:00Z\"\n - number: 110\n title: \"Issue title number 10: something broke, badly\"\n state: open\n user:\n login: user10\n id: 1010\n labels[2]: bug,p1\n comments: 20\n created_at: \"2026-06-11T10:00:00Z\"\n - number: 111\n title: \"Issue title number 11: something broke, badly\"\n state: open\n user:\n login: user11\n id: 1011\n labels[2]: bug,p2\n comments: 22\n created_at: \"2026-06-12T10:00:00Z\"\ntotal_count: 12" + } + }, + { + "name": "realistic-db-rows", + "rawContent": "[{\"id\":0,\"email\":\"person0@example.com\",\"balance\":0,\"active\":true,\"region\":\"us-east\"},{\"id\":1,\"email\":\"person1@example.com\",\"balance\":137.13,\"active\":false,\"region\":\"eu-west\"},{\"id\":2,\"email\":\"person2@example.com\",\"balance\":274.26,\"active\":true,\"region\":\"ap-south\"},{\"id\":3,\"email\":\"person3@example.com\",\"balance\":411.39,\"active\":false,\"region\":\"us-east\"},{\"id\":4,\"email\":\"person4@example.com\",\"balance\":548.52,\"active\":true,\"region\":\"eu-west\"},{\"id\":5,\"email\":\"person5@example.com\",\"balance\":685.65,\"active\":false,\"region\":\"ap-south\"},{\"id\":6,\"email\":\"person6@example.com\",\"balance\":822.78,\"active\":true,\"region\":\"us-east\"},{\"id\":7,\"email\":\"person7@example.com\",\"balance\":959.91,\"active\":false,\"region\":\"eu-west\"},{\"id\":8,\"email\":\"person8@example.com\",\"balance\":97.04,\"active\":true,\"region\":\"ap-south\"},{\"id\":9,\"email\":\"person9@example.com\",\"balance\":234.17,\"active\":false,\"region\":\"us-east\"},{\"id\":10,\"email\":\"person10@example.com\",\"balance\":371.3,\"active\":true,\"region\":\"eu-west\"},{\"id\":11,\"email\":\"person11@example.com\",\"balance\":508.43,\"active\":false,\"region\":\"ap-south\"},{\"id\":12,\"email\":\"person12@example.com\",\"balance\":645.56,\"active\":true,\"region\":\"us-east\"},{\"id\":13,\"email\":\"person13@example.com\",\"balance\":782.69,\"active\":false,\"region\":\"eu-west\"},{\"id\":14,\"email\":\"person14@example.com\",\"balance\":919.82,\"active\":true,\"region\":\"ap-south\"},{\"id\":15,\"email\":\"person15@example.com\",\"balance\":56.95,\"active\":false,\"region\":\"us-east\"},{\"id\":16,\"email\":\"person16@example.com\",\"balance\":194.08,\"active\":true,\"region\":\"eu-west\"},{\"id\":17,\"email\":\"person17@example.com\",\"balance\":331.21,\"active\":false,\"region\":\"ap-south\"},{\"id\":18,\"email\":\"person18@example.com\",\"balance\":468.34,\"active\":true,\"region\":\"us-east\"},{\"id\":19,\"email\":\"person19@example.com\",\"balance\":605.47,\"active\":false,\"region\":\"eu-west\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"email\":\"person0@example.com\",\"balance\":0,\"active\":true,\"region\":\"us-east\"},{\"id\":1,\"email\":\"person1@example.com\",\"balance\":137.13,\"active\":false,\"region\":\"eu-west\"},{\"id\":2,\"email\":\"person2@example.com\",\"balance\":274.26,\"active\":true,\"region\":\"ap-south\"},{\"id\":3,\"email\":\"person3@example.com\",\"balance\":411.39,\"active\":false,\"region\":\"us-east\"},{\"id\":4,\"email\":\"person4@example.com\",\"balance\":548.52,\"active\":true,\"region\":\"eu-west\"},{\"id\":5,\"email\":\"person5@example.com\",\"balance\":685.65,\"active\":false,\"region\":\"ap-south\"},{\"id\":6,\"email\":\"person6@example.com\",\"balance\":822.78,\"active\":true,\"region\":\"us-east\"},{\"id\":7,\"email\":\"person7@example.com\",\"balance\":959.91,\"active\":false,\"region\":\"eu-west\"},{\"id\":8,\"email\":\"person8@example.com\",\"balance\":97.04,\"active\":true,\"region\":\"ap-south\"},{\"id\":9,\"email\":\"person9@example.com\",\"balance\":234.17,\"active\":false,\"region\":\"us-east\"},{\"id\":10,\"email\":\"person10@example.com\",\"balance\":371.3,\"active\":true,\"region\":\"eu-west\"},{\"id\":11,\"email\":\"person11@example.com\",\"balance\":508.43,\"active\":false,\"region\":\"ap-south\"},{\"id\":12,\"email\":\"person12@example.com\",\"balance\":645.56,\"active\":true,\"region\":\"us-east\"},{\"id\":13,\"email\":\"person13@example.com\",\"balance\":782.69,\"active\":false,\"region\":\"eu-west\"},{\"id\":14,\"email\":\"person14@example.com\",\"balance\":919.82,\"active\":true,\"region\":\"ap-south\"},{\"id\":15,\"email\":\"person15@example.com\",\"balance\":56.95,\"active\":false,\"region\":\"us-east\"},{\"id\":16,\"email\":\"person16@example.com\",\"balance\":194.08,\"active\":true,\"region\":\"eu-west\"},{\"id\":17,\"email\":\"person17@example.com\",\"balance\":331.21,\"active\":false,\"region\":\"ap-south\"},{\"id\":18,\"email\":\"person18@example.com\",\"balance\":468.34,\"active\":true,\"region\":\"us-east\"},{\"id\":19,\"email\":\"person19@example.com\",\"balance\":605.47,\"active\":false,\"region\":\"eu-west\"}]", + "encoded": "[20]{id,email,balance,active,region}:\n 0,person0@example.com,0,true,\"us-east\"\n 1,person1@example.com,137.13,false,\"eu-west\"\n 2,person2@example.com,274.26,true,\"ap-south\"\n 3,person3@example.com,411.39,false,\"us-east\"\n 4,person4@example.com,548.52,true,\"eu-west\"\n 5,person5@example.com,685.65,false,\"ap-south\"\n 6,person6@example.com,822.78,true,\"us-east\"\n 7,person7@example.com,959.91,false,\"eu-west\"\n 8,person8@example.com,97.04,true,\"ap-south\"\n 9,person9@example.com,234.17,false,\"us-east\"\n 10,person10@example.com,371.3,true,\"eu-west\"\n 11,person11@example.com,508.43,false,\"ap-south\"\n 12,person12@example.com,645.56,true,\"us-east\"\n 13,person13@example.com,782.69,false,\"eu-west\"\n 14,person14@example.com,919.82,true,\"ap-south\"\n 15,person15@example.com,56.95,false,\"us-east\"\n 16,person16@example.com,194.08,true,\"eu-west\"\n 17,person17@example.com,331.21,false,\"ap-south\"\n 18,person18@example.com,468.34,true,\"us-east\"\n 19,person19@example.com,605.47,false,\"eu-west\"" + } + }, + { + "name": "bench-1kb-0", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-601103\",\"name\":\"kilo uniform\",\"status\":\"archived\",\"score\":17.48,\"quantity\":263,\"active\":false,\"updatedAt\":\"2026-04-25T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-472317\",\"name\":\"foxtrot victor\",\"status\":\"archived\",\"score\":30.7,\"quantity\":98,\"active\":true,\"updatedAt\":\"2026-05-18T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-3842\",\"name\":\"lima uniform\",\"status\":\"active\",\"score\":59.23,\"quantity\":15,\"active\":false,\"updatedAt\":\"2026-01-06T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-783547\",\"name\":\"mike alpha\",\"status\":\"active\",\"score\":84.27,\"quantity\":243,\"active\":true,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-37439\",\"name\":\"bravo november\",\"status\":\"archived\",\"score\":24.52,\"quantity\":322,\"active\":false,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-858710\",\"name\":\"mike echo\",\"status\":\"pending\",\"score\":29.3,\"quantity\":37,\"active\":true,\"updatedAt\":\"2026-05-20T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-927839\",\"name\":\"charlie whiskey\",\"status\":\"pending\",\"score\":94.23,\"quantity\":68,\"active\":false,\"updatedAt\":\"2026-01-11T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-601103\",\"name\":\"kilo uniform\",\"status\":\"archived\",\"score\":17.48,\"quantity\":263,\"active\":false,\"updatedAt\":\"2026-04-25T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-472317\",\"name\":\"foxtrot victor\",\"status\":\"archived\",\"score\":30.7,\"quantity\":98,\"active\":true,\"updatedAt\":\"2026-05-18T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-3842\",\"name\":\"lima uniform\",\"status\":\"active\",\"score\":59.23,\"quantity\":15,\"active\":false,\"updatedAt\":\"2026-01-06T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-783547\",\"name\":\"mike alpha\",\"status\":\"active\",\"score\":84.27,\"quantity\":243,\"active\":true,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-37439\",\"name\":\"bravo november\",\"status\":\"archived\",\"score\":24.52,\"quantity\":322,\"active\":false,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-858710\",\"name\":\"mike echo\",\"status\":\"pending\",\"score\":29.3,\"quantity\":37,\"active\":true,\"updatedAt\":\"2026-05-20T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-927839\",\"name\":\"charlie whiskey\",\"status\":\"pending\",\"score\":94.23,\"quantity\":68,\"active\":false,\"updatedAt\":\"2026-01-11T12:00:00Z\"}]", + "encoded": "[7]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-601103\",kilo uniform,archived,17.48,263,false,\"2026-04-25T12:00:00Z\"\n 1,\"SKU-472317\",foxtrot victor,archived,30.7,98,true,\"2026-05-18T12:00:00Z\"\n 2,\"SKU-3842\",lima uniform,active,59.23,15,false,\"2026-01-06T12:00:00Z\"\n 3,\"SKU-783547\",mike alpha,active,84.27,243,true,\"2026-02-13T12:00:00Z\"\n 4,\"SKU-37439\",bravo november,archived,24.52,322,false,\"2026-02-21T12:00:00Z\"\n 5,\"SKU-858710\",mike echo,pending,29.3,37,true,\"2026-05-20T12:00:00Z\"\n 6,\"SKU-927839\",charlie whiskey,pending,94.23,68,false,\"2026-01-11T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-1", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-481722\",\"name\":\"oscar victor\",\"status\":\"active\",\"score\":71.96,\"quantity\":467,\"active\":false,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-520316\",\"name\":\"tango charlie\",\"status\":\"failed\",\"score\":56.28,\"quantity\":458,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-596239\",\"name\":\"hotel romeo\",\"status\":\"pending\",\"score\":44.98,\"quantity\":421,\"active\":true,\"updatedAt\":\"2026-06-25T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-431891\",\"name\":\"november hotel\",\"status\":\"active\",\"score\":69.67,\"quantity\":156,\"active\":true,\"updatedAt\":\"2026-06-03T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-475391\",\"name\":\"tango delta\",\"status\":\"failed\",\"score\":17.84,\"quantity\":354,\"active\":true,\"updatedAt\":\"2026-02-12T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-163437\",\"name\":\"papa india\",\"status\":\"active\",\"score\":62.89,\"quantity\":443,\"active\":false,\"updatedAt\":\"2026-05-12T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-131392\",\"name\":\"uniform india\",\"status\":\"active\",\"score\":73.74,\"quantity\":317,\"active\":true,\"updatedAt\":\"2026-06-06T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-481722\",\"name\":\"oscar victor\",\"status\":\"active\",\"score\":71.96,\"quantity\":467,\"active\":false,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-520316\",\"name\":\"tango charlie\",\"status\":\"failed\",\"score\":56.28,\"quantity\":458,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-596239\",\"name\":\"hotel romeo\",\"status\":\"pending\",\"score\":44.98,\"quantity\":421,\"active\":true,\"updatedAt\":\"2026-06-25T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-431891\",\"name\":\"november hotel\",\"status\":\"active\",\"score\":69.67,\"quantity\":156,\"active\":true,\"updatedAt\":\"2026-06-03T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-475391\",\"name\":\"tango delta\",\"status\":\"failed\",\"score\":17.84,\"quantity\":354,\"active\":true,\"updatedAt\":\"2026-02-12T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-163437\",\"name\":\"papa india\",\"status\":\"active\",\"score\":62.89,\"quantity\":443,\"active\":false,\"updatedAt\":\"2026-05-12T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-131392\",\"name\":\"uniform india\",\"status\":\"active\",\"score\":73.74,\"quantity\":317,\"active\":true,\"updatedAt\":\"2026-06-06T12:00:00Z\"}]", + "encoded": "[7]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-481722\",oscar victor,active,71.96,467,false,\"2026-05-06T12:00:00Z\"\n 1,\"SKU-520316\",tango charlie,failed,56.28,458,false,\"2026-04-10T12:00:00Z\"\n 2,\"SKU-596239\",hotel romeo,pending,44.98,421,true,\"2026-06-25T12:00:00Z\"\n 3,\"SKU-431891\",november hotel,active,69.67,156,true,\"2026-06-03T12:00:00Z\"\n 4,\"SKU-475391\",tango delta,failed,17.84,354,true,\"2026-02-12T12:00:00Z\"\n 5,\"SKU-163437\",papa india,active,62.89,443,false,\"2026-05-12T12:00:00Z\"\n 6,\"SKU-131392\",uniform india,active,73.74,317,true,\"2026-06-06T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-2", + "rawContent": "{\"meta\":{\"source\":\"bench\",\"version\":3,\"total\":5},\"entries\":{\"entry_0\":{\"id\":0,\"sku\":\"SKU-603932\",\"name\":\"lima echo\",\"status\":\"active\",\"score\":70.56,\"quantity\":354,\"active\":false,\"updatedAt\":\"2026-02-10T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"foxtrot\"],\"depth\":2}},\"entry_1\":{\"id\":1,\"sku\":\"SKU-993520\",\"name\":\"november zulu\",\"status\":\"active\",\"score\":17.08,\"quantity\":280,\"active\":false,\"updatedAt\":\"2026-03-13T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"quebec\"],\"depth\":2}},\"entry_2\":{\"id\":2,\"sku\":\"SKU-398513\",\"name\":\"india victor\",\"status\":\"pending\",\"score\":49.53,\"quantity\":305,\"active\":true,\"updatedAt\":\"2026-04-07T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"hotel\"],\"depth\":2}},\"entry_3\":{\"id\":3,\"sku\":\"SKU-320565\",\"name\":\"quebec foxtrot\",\"status\":\"active\",\"score\":69.5,\"quantity\":157,\"active\":false,\"updatedAt\":\"2026-02-02T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"echo\"],\"depth\":2}},\"entry_4\":{\"id\":4,\"sku\":\"SKU-481534\",\"name\":\"juliet romeo\",\"status\":\"failed\",\"score\":81.17,\"quantity\":219,\"active\":false,\"updatedAt\":\"2026-06-15T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"victor\"],\"depth\":2}}}}", + "unwrap": true, + "expected": { + "normalized": "{\"meta\":{\"source\":\"bench\",\"version\":3,\"total\":5},\"entries\":{\"entry_0\":{\"id\":0,\"sku\":\"SKU-603932\",\"name\":\"lima echo\",\"status\":\"active\",\"score\":70.56,\"quantity\":354,\"active\":false,\"updatedAt\":\"2026-02-10T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"foxtrot\"],\"depth\":2}},\"entry_1\":{\"id\":1,\"sku\":\"SKU-993520\",\"name\":\"november zulu\",\"status\":\"active\",\"score\":17.08,\"quantity\":280,\"active\":false,\"updatedAt\":\"2026-03-13T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"quebec\"],\"depth\":2}},\"entry_2\":{\"id\":2,\"sku\":\"SKU-398513\",\"name\":\"india victor\",\"status\":\"pending\",\"score\":49.53,\"quantity\":305,\"active\":true,\"updatedAt\":\"2026-04-07T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"hotel\"],\"depth\":2}},\"entry_3\":{\"id\":3,\"sku\":\"SKU-320565\",\"name\":\"quebec foxtrot\",\"status\":\"active\",\"score\":69.5,\"quantity\":157,\"active\":false,\"updatedAt\":\"2026-02-02T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"echo\"],\"depth\":2}},\"entry_4\":{\"id\":4,\"sku\":\"SKU-481534\",\"name\":\"juliet romeo\",\"status\":\"failed\",\"score\":81.17,\"quantity\":219,\"active\":false,\"updatedAt\":\"2026-06-15T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"victor\"],\"depth\":2}}}}", + "encoded": "meta:\n source: bench\n version: 3\n total: 5\nentries:\n entry_0:\n id: 0\n sku: \"SKU-603932\"\n name: lima echo\n status: active\n score: 70.56\n quantity: 354\n active: false\n updatedAt: \"2026-02-10T12:00:00Z\"\n nested:\n tags[2]: alpha,foxtrot\n depth: 2\n entry_1:\n id: 1\n sku: \"SKU-993520\"\n name: november zulu\n status: active\n score: 17.08\n quantity: 280\n active: false\n updatedAt: \"2026-03-13T12:00:00Z\"\n nested:\n tags[2]: kilo,quebec\n depth: 2\n entry_2:\n id: 2\n sku: \"SKU-398513\"\n name: india victor\n status: pending\n score: 49.53\n quantity: 305\n active: true\n updatedAt: \"2026-04-07T12:00:00Z\"\n nested:\n tags[2]: hotel,hotel\n depth: 2\n entry_3:\n id: 3\n sku: \"SKU-320565\"\n name: quebec foxtrot\n status: active\n score: 69.5\n quantity: 157\n active: false\n updatedAt: \"2026-02-02T12:00:00Z\"\n nested:\n tags[2]: november,echo\n depth: 2\n entry_4:\n id: 4\n sku: \"SKU-481534\"\n name: juliet romeo\n status: failed\n score: 81.17\n quantity: 219\n active: false\n updatedAt: \"2026-06-15T12:00:00Z\"\n nested:\n tags[2]: uniform,victor\n depth: 2" + } + }, + { + "name": "bench-1kb-3", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-450102\",\"name\":\"delta oscar\",\"status\":\"failed\",\"score\":20.84,\"quantity\":194,\"active\":true,\"updatedAt\":\"2026-06-14T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-859482\",\"name\":\"uniform romeo\",\"status\":\"active\",\"score\":6.93,\"quantity\":84,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-991456\",\"name\":\"tango papa\",\"status\":\"failed\",\"score\":85.3,\"quantity\":299,\"active\":false,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-680988\",\"name\":\"india whiskey\",\"status\":\"archived\",\"score\":32.93,\"quantity\":422,\"active\":false,\"updatedAt\":\"2026-03-01T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-85502\",\"name\":\"papa romeo\",\"status\":\"failed\",\"score\":87.95,\"quantity\":233,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-139138\",\"name\":\"bravo golf\",\"status\":\"pending\",\"score\":56.88,\"quantity\":358,\"active\":true,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-666356\",\"name\":\"mike kilo\",\"status\":\"pending\",\"score\":45.91,\"quantity\":212,\"active\":false,\"updatedAt\":\"2026-02-05T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-504597\",\"name\":\"golf mike\",\"status\":\"archived\",\"score\":64.74,\"quantity\":218,\"active\":false,\"updatedAt\":\"2026-01-05T12:00:00Z\"}]", + "unwrap": false, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-450102\",\"name\":\"delta oscar\",\"status\":\"failed\",\"score\":20.84,\"quantity\":194,\"active\":true,\"updatedAt\":\"2026-06-14T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-859482\",\"name\":\"uniform romeo\",\"status\":\"active\",\"score\":6.93,\"quantity\":84,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-991456\",\"name\":\"tango papa\",\"status\":\"failed\",\"score\":85.3,\"quantity\":299,\"active\":false,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-680988\",\"name\":\"india whiskey\",\"status\":\"archived\",\"score\":32.93,\"quantity\":422,\"active\":false,\"updatedAt\":\"2026-03-01T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-85502\",\"name\":\"papa romeo\",\"status\":\"failed\",\"score\":87.95,\"quantity\":233,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-139138\",\"name\":\"bravo golf\",\"status\":\"pending\",\"score\":56.88,\"quantity\":358,\"active\":true,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-666356\",\"name\":\"mike kilo\",\"status\":\"pending\",\"score\":45.91,\"quantity\":212,\"active\":false,\"updatedAt\":\"2026-02-05T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-504597\",\"name\":\"golf mike\",\"status\":\"archived\",\"score\":64.74,\"quantity\":218,\"active\":false,\"updatedAt\":\"2026-01-05T12:00:00Z\"}]", + "encoded": "[8]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-450102\",delta oscar,failed,20.84,194,true,\"2026-06-14T12:00:00Z\"\n 1,\"SKU-859482\",uniform romeo,active,6.93,84,true,\"2026-05-22T12:00:00Z\"\n 2,\"SKU-991456\",tango papa,failed,85.3,299,false,\"2026-03-25T12:00:00Z\"\n 3,\"SKU-680988\",india whiskey,archived,32.93,422,false,\"2026-03-01T12:00:00Z\"\n 4,\"SKU-85502\",papa romeo,failed,87.95,233,true,\"2026-01-08T12:00:00Z\"\n 5,\"SKU-139138\",bravo golf,pending,56.88,358,true,\"2026-05-06T12:00:00Z\"\n 6,\"SKU-666356\",mike kilo,pending,45.91,212,false,\"2026-02-05T12:00:00Z\"\n 7,\"SKU-504597\",golf mike,archived,64.74,218,false,\"2026-01-05T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-4", + "rawContent": "Tool run 943 output: bravo uniform kilo mike charlie tango bravo india foxtrot quebec november charlie zulu sierra papa tango november tango mike echo bravo charlie bravo golf quebec quebec november zulu alpha juliet india golf tango mike echo whiskey whiskey golf mike uniform whiskey quebec november tango hotel lima sierra november quebec november november zulu zulu sierra hotel papa quebec uniform juliet victor hotel victor juliet india mike uniform oscar uniform quebec delta golf zulu lima golf papa sierra romeo romeo echo uniform victor november lima bravo lima victor quebec kilo kilo oscar romeo mike bravo echo charlie charlie juliet tango uniform quebec charlie alpha golf quebec golf uniform papa victor victor india charlie papa foxtrot lima golf golf romeo mike lima oscar charlie sierra quebec whiskey charlie bravo alpha india foxtrot foxtrot zulu november uniform india papa alpha romeo tango juliet echo sierra mike delta bravo sierra kilo uniform tango lima whiskey echo delta november quebec echo sierra", + "unwrap": true, + "expected": { + "normalized": "Tool run 943 output: bravo uniform kilo mike charlie tango bravo india foxtrot quebec november charlie zulu sierra papa tango november tango mike echo bravo charlie bravo golf quebec quebec november zulu alpha juliet india golf tango mike echo whiskey whiskey golf mike uniform whiskey quebec november tango hotel lima sierra november quebec november november zulu zulu sierra hotel papa quebec uniform juliet victor hotel victor juliet india mike uniform oscar uniform quebec delta golf zulu lima golf papa sierra romeo romeo echo uniform victor november lima bravo lima victor quebec kilo kilo oscar romeo mike bravo echo charlie charlie juliet tango uniform quebec charlie alpha golf quebec golf uniform papa victor victor india charlie papa foxtrot lima golf golf romeo mike lima oscar charlie sierra quebec whiskey charlie bravo alpha india foxtrot foxtrot zulu november uniform india papa alpha romeo tango juliet echo sierra mike delta bravo sierra kilo uniform tango lima whiskey echo delta november quebec echo sierra", + "encoded": null + } + }, + { + "name": "bench-1kb-5", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-241297\",\"name\":\"delta papa\",\"status\":\"archived\",\"score\":55.54,\"quantity\":102,\"active\":true,\"updatedAt\":\"2026-04-21T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-49849\",\"name\":\"whiskey november\",\"status\":\"pending\",\"score\":4.71,\"quantity\":163,\"active\":true,\"updatedAt\":\"2026-05-08T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-862775\",\"name\":\"kilo echo\",\"status\":\"archived\",\"score\":50.75,\"quantity\":94,\"active\":true,\"updatedAt\":\"2026-01-04T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-994988\",\"name\":\"foxtrot lima\",\"status\":\"archived\",\"score\":61.96,\"quantity\":242,\"active\":true,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-829234\",\"name\":\"india echo\",\"status\":\"archived\",\"score\":76.22,\"quantity\":222,\"active\":false,\"updatedAt\":\"2026-02-14T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-34817\",\"name\":\"uniform india\",\"status\":\"pending\",\"score\":25.15,\"quantity\":251,\"active\":false,\"updatedAt\":\"2026-04-02T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-901145\",\"name\":\"sierra papa\",\"status\":\"failed\",\"score\":5.38,\"quantity\":293,\"active\":true,\"updatedAt\":\"2026-03-15T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-241297\",\"name\":\"delta papa\",\"status\":\"archived\",\"score\":55.54,\"quantity\":102,\"active\":true,\"updatedAt\":\"2026-04-21T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-49849\",\"name\":\"whiskey november\",\"status\":\"pending\",\"score\":4.71,\"quantity\":163,\"active\":true,\"updatedAt\":\"2026-05-08T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-862775\",\"name\":\"kilo echo\",\"status\":\"archived\",\"score\":50.75,\"quantity\":94,\"active\":true,\"updatedAt\":\"2026-01-04T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-994988\",\"name\":\"foxtrot lima\",\"status\":\"archived\",\"score\":61.96,\"quantity\":242,\"active\":true,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-829234\",\"name\":\"india echo\",\"status\":\"archived\",\"score\":76.22,\"quantity\":222,\"active\":false,\"updatedAt\":\"2026-02-14T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-34817\",\"name\":\"uniform india\",\"status\":\"pending\",\"score\":25.15,\"quantity\":251,\"active\":false,\"updatedAt\":\"2026-04-02T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-901145\",\"name\":\"sierra papa\",\"status\":\"failed\",\"score\":5.38,\"quantity\":293,\"active\":true,\"updatedAt\":\"2026-03-15T12:00:00Z\"}]", + "encoded": "[7]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-241297\",delta papa,archived,55.54,102,true,\"2026-04-21T12:00:00Z\"\n 1,\"SKU-49849\",whiskey november,pending,4.71,163,true,\"2026-05-08T12:00:00Z\"\n 2,\"SKU-862775\",kilo echo,archived,50.75,94,true,\"2026-01-04T12:00:00Z\"\n 3,\"SKU-994988\",foxtrot lima,archived,61.96,242,true,\"2026-01-28T12:00:00Z\"\n 4,\"SKU-829234\",india echo,archived,76.22,222,false,\"2026-02-14T12:00:00Z\"\n 5,\"SKU-34817\",uniform india,pending,25.15,251,false,\"2026-04-02T12:00:00Z\"\n 6,\"SKU-901145\",sierra papa,failed,5.38,293,true,\"2026-03-15T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-6", + "rawContent": "[{\"type\":\"text\",\"text\":\"[{\\\"id\\\":0,\\\"sku\\\":\\\"SKU-475386\\\",\\\"name\\\":\\\"kilo november\\\",\\\"status\\\":\\\"active\\\",\\\"score\\\":70.83,\\\"quantity\\\":1,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-02-11T12:00:00Z\\\"},{\\\"id\\\":1,\\\"sku\\\":\\\"SKU-725772\\\",\\\"name\\\":\\\"whiskey golf\\\",\\\"status\\\":\\\"archived\\\",\\\"score\\\":90.26,\\\"quantity\\\":231,\\\"active\\\":false,\\\"updatedAt\\\":\\\"2026-04-14T12:00:00Z\\\"},{\\\"id\\\":2,\\\"sku\\\":\\\"SKU-442017\\\",\\\"name\\\":\\\"lima sierra\\\",\\\"status\\\":\\\"active\\\",\\\"score\\\":71.48,\\\"quantity\\\":297,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-05-14T12:00:00Z\\\"},{\\\"id\\\":3,\\\"sku\\\":\\\"SKU-8258\\\",\\\"name\\\":\\\"victor bravo\\\",\\\"status\\\":\\\"archived\\\",\\\"score\\\":48.68,\\\"quantity\\\":340,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-04-24T12:00:00Z\\\"},{\\\"id\\\":4,\\\"sku\\\":\\\"SKU-664289\\\",\\\"name\\\":\\\"zulu zulu\\\",\\\"status\\\":\\\"failed\\\",\\\"score\\\":81.17,\\\"quantity\\\":478,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-01-03T12:00:00Z\\\"},{\\\"id\\\":5,\\\"sku\\\":\\\"SKU-992953\\\",\\\"name\\\":\\\"alpha lima\\\",\\\"status\\\":\\\"failed\\\",\\\"score\\\":67.92,\\\"quantity\\\":388,\\\"active\\\":false,\\\"updatedAt\\\":\\\"2026-06-13T12:00:00Z\\\"},{\\\"id\\\":6,\\\"sku\\\":\\\"SKU-923099\\\",\\\"name\\\":\\\"papa golf\\\",\\\"status\\\":\\\"active\\\",\\\"score\\\":37.45,\\\"quantity\\\":362,\\\"active\\\":false,\\\"updatedAt\\\":\\\"2026-01-07T12:00:00Z\\\"},{\\\"id\\\":7,\\\"sku\\\":\\\"SKU-620884\\\",\\\"name\\\":\\\"india papa\\\",\\\"status\\\":\\\"failed\\\",\\\"score\\\":40.95,\\\"quantity\\\":396,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-01-20T12:00:00Z\\\"}]\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-475386\",\"name\":\"kilo november\",\"status\":\"active\",\"score\":70.83,\"quantity\":1,\"active\":true,\"updatedAt\":\"2026-02-11T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-725772\",\"name\":\"whiskey golf\",\"status\":\"archived\",\"score\":90.26,\"quantity\":231,\"active\":false,\"updatedAt\":\"2026-04-14T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-442017\",\"name\":\"lima sierra\",\"status\":\"active\",\"score\":71.48,\"quantity\":297,\"active\":true,\"updatedAt\":\"2026-05-14T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-8258\",\"name\":\"victor bravo\",\"status\":\"archived\",\"score\":48.68,\"quantity\":340,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-664289\",\"name\":\"zulu zulu\",\"status\":\"failed\",\"score\":81.17,\"quantity\":478,\"active\":true,\"updatedAt\":\"2026-01-03T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-992953\",\"name\":\"alpha lima\",\"status\":\"failed\",\"score\":67.92,\"quantity\":388,\"active\":false,\"updatedAt\":\"2026-06-13T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-923099\",\"name\":\"papa golf\",\"status\":\"active\",\"score\":37.45,\"quantity\":362,\"active\":false,\"updatedAt\":\"2026-01-07T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-620884\",\"name\":\"india papa\",\"status\":\"failed\",\"score\":40.95,\"quantity\":396,\"active\":true,\"updatedAt\":\"2026-01-20T12:00:00Z\"}]", + "encoded": "[8]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-475386\",kilo november,active,70.83,1,true,\"2026-02-11T12:00:00Z\"\n 1,\"SKU-725772\",whiskey golf,archived,90.26,231,false,\"2026-04-14T12:00:00Z\"\n 2,\"SKU-442017\",lima sierra,active,71.48,297,true,\"2026-05-14T12:00:00Z\"\n 3,\"SKU-8258\",victor bravo,archived,48.68,340,true,\"2026-04-24T12:00:00Z\"\n 4,\"SKU-664289\",zulu zulu,failed,81.17,478,true,\"2026-01-03T12:00:00Z\"\n 5,\"SKU-992953\",alpha lima,failed,67.92,388,false,\"2026-06-13T12:00:00Z\"\n 6,\"SKU-923099\",papa golf,active,37.45,362,false,\"2026-01-07T12:00:00Z\"\n 7,\"SKU-620884\",india papa,failed,40.95,396,true,\"2026-01-20T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-7", + "rawContent": "[{\"type\":\"text\",\"text\":\"{\\\"meta\\\":{\\\"source\\\":\\\"bench\\\",\\\"version\\\":3,\\\"total\\\":5},\\\"entries\\\":{\\\"entry_0\\\":{\\\"id\\\":0,\\\"sku\\\":\\\"SKU-875281\\\",\\\"name\\\":\\\"golf oscar\\\",\\\"status\\\":\\\"failed\\\",\\\"score\\\":46.69,\\\"quantity\\\":190,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-04-09T12:00:00Z\\\",\\\"nested\\\":{\\\"tags\\\":[\\\"lima\\\",\\\"mike\\\"],\\\"depth\\\":2}},\\\"entry_1\\\":{\\\"id\\\":1,\\\"sku\\\":\\\"SKU-995847\\\",\\\"name\\\":\\\"echo oscar\\\",\\\"status\\\":\\\"active\\\",\\\"score\\\":64.41,\\\"quantity\\\":397,\\\"active\\\":false,\\\"updatedAt\\\":\\\"2026-04-24T12:00:00Z\\\",\\\"nested\\\":{\\\"tags\\\":[\\\"juliet\\\",\\\"oscar\\\"],\\\"depth\\\":2}},\\\"entry_2\\\":{\\\"id\\\":2,\\\"sku\\\":\\\"SKU-169022\\\",\\\"name\\\":\\\"zulu echo\\\",\\\"status\\\":\\\"archived\\\",\\\"score\\\":70.82,\\\"quantity\\\":283,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-04-26T12:00:00Z\\\",\\\"nested\\\":{\\\"tags\\\":[\\\"sierra\\\",\\\"alpha\\\"],\\\"depth\\\":2}},\\\"entry_3\\\":{\\\"id\\\":3,\\\"sku\\\":\\\"SKU-60400\\\",\\\"name\\\":\\\"charlie victor\\\",\\\"status\\\":\\\"active\\\",\\\"score\\\":3.47,\\\"quantity\\\":261,\\\"active\\\":false,\\\"updatedAt\\\":\\\"2026-05-18T12:00:00Z\\\",\\\"nested\\\":{\\\"tags\\\":[\\\"alpha\\\",\\\"juliet\\\"],\\\"depth\\\":2}},\\\"entry_4\\\":{\\\"id\\\":4,\\\"sku\\\":\\\"SKU-825996\\\",\\\"name\\\":\\\"india echo\\\",\\\"status\\\":\\\"archived\\\",\\\"score\\\":65.27,\\\"quantity\\\":24,\\\"active\\\":true,\\\"updatedAt\\\":\\\"2026-04-24T12:00:00Z\\\",\\\"nested\\\":{\\\"tags\\\":[\\\"november\\\",\\\"juliet\\\"],\\\"depth\\\":2}}}}\"}]", + "unwrap": true, + "expected": { + "normalized": "{\"meta\":{\"source\":\"bench\",\"version\":3,\"total\":5},\"entries\":{\"entry_0\":{\"id\":0,\"sku\":\"SKU-875281\",\"name\":\"golf oscar\",\"status\":\"failed\",\"score\":46.69,\"quantity\":190,\"active\":true,\"updatedAt\":\"2026-04-09T12:00:00Z\",\"nested\":{\"tags\":[\"lima\",\"mike\"],\"depth\":2}},\"entry_1\":{\"id\":1,\"sku\":\"SKU-995847\",\"name\":\"echo oscar\",\"status\":\"active\",\"score\":64.41,\"quantity\":397,\"active\":false,\"updatedAt\":\"2026-04-24T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"oscar\"],\"depth\":2}},\"entry_2\":{\"id\":2,\"sku\":\"SKU-169022\",\"name\":\"zulu echo\",\"status\":\"archived\",\"score\":70.82,\"quantity\":283,\"active\":true,\"updatedAt\":\"2026-04-26T12:00:00Z\",\"nested\":{\"tags\":[\"sierra\",\"alpha\"],\"depth\":2}},\"entry_3\":{\"id\":3,\"sku\":\"SKU-60400\",\"name\":\"charlie victor\",\"status\":\"active\",\"score\":3.47,\"quantity\":261,\"active\":false,\"updatedAt\":\"2026-05-18T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"juliet\"],\"depth\":2}},\"entry_4\":{\"id\":4,\"sku\":\"SKU-825996\",\"name\":\"india echo\",\"status\":\"archived\",\"score\":65.27,\"quantity\":24,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"juliet\"],\"depth\":2}}}}", + "encoded": "meta:\n source: bench\n version: 3\n total: 5\nentries:\n entry_0:\n id: 0\n sku: \"SKU-875281\"\n name: golf oscar\n status: failed\n score: 46.69\n quantity: 190\n active: true\n updatedAt: \"2026-04-09T12:00:00Z\"\n nested:\n tags[2]: lima,mike\n depth: 2\n entry_1:\n id: 1\n sku: \"SKU-995847\"\n name: echo oscar\n status: active\n score: 64.41\n quantity: 397\n active: false\n updatedAt: \"2026-04-24T12:00:00Z\"\n nested:\n tags[2]: juliet,oscar\n depth: 2\n entry_2:\n id: 2\n sku: \"SKU-169022\"\n name: zulu echo\n status: archived\n score: 70.82\n quantity: 283\n active: true\n updatedAt: \"2026-04-26T12:00:00Z\"\n nested:\n tags[2]: sierra,alpha\n depth: 2\n entry_3:\n id: 3\n sku: \"SKU-60400\"\n name: charlie victor\n status: active\n score: 3.47\n quantity: 261\n active: false\n updatedAt: \"2026-05-18T12:00:00Z\"\n nested:\n tags[2]: alpha,juliet\n depth: 2\n entry_4:\n id: 4\n sku: \"SKU-825996\"\n name: india echo\n status: archived\n score: 65.27\n quantity: 24\n active: true\n updatedAt: \"2026-04-24T12:00:00Z\"\n nested:\n tags[2]: november,juliet\n depth: 2" + } + }, + { + "name": "bench-1kb-8", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-588157\",\"name\":\"juliet oscar\",\"status\":\"active\",\"score\":13.52,\"quantity\":353,\"active\":true,\"updatedAt\":\"2026-02-26T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-943313\",\"name\":\"kilo oscar\",\"status\":\"pending\",\"score\":66.62,\"quantity\":489,\"active\":true,\"updatedAt\":\"2026-05-10T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-909215\",\"name\":\"mike whiskey\",\"status\":\"active\",\"score\":15.08,\"quantity\":159,\"active\":true,\"updatedAt\":\"2026-04-28T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-21248\",\"name\":\"papa oscar\",\"status\":\"failed\",\"score\":25.06,\"quantity\":481,\"active\":false,\"updatedAt\":\"2026-06-09T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-512470\",\"name\":\"juliet foxtrot\",\"status\":\"pending\",\"score\":94.43,\"quantity\":313,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-445960\",\"name\":\"echo november\",\"status\":\"failed\",\"score\":65.91,\"quantity\":270,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-443750\",\"name\":\"papa kilo\",\"status\":\"archived\",\"score\":66.03,\"quantity\":312,\"active\":false,\"updatedAt\":\"2026-02-01T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-588157\",\"name\":\"juliet oscar\",\"status\":\"active\",\"score\":13.52,\"quantity\":353,\"active\":true,\"updatedAt\":\"2026-02-26T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-943313\",\"name\":\"kilo oscar\",\"status\":\"pending\",\"score\":66.62,\"quantity\":489,\"active\":true,\"updatedAt\":\"2026-05-10T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-909215\",\"name\":\"mike whiskey\",\"status\":\"active\",\"score\":15.08,\"quantity\":159,\"active\":true,\"updatedAt\":\"2026-04-28T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-21248\",\"name\":\"papa oscar\",\"status\":\"failed\",\"score\":25.06,\"quantity\":481,\"active\":false,\"updatedAt\":\"2026-06-09T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-512470\",\"name\":\"juliet foxtrot\",\"status\":\"pending\",\"score\":94.43,\"quantity\":313,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-445960\",\"name\":\"echo november\",\"status\":\"failed\",\"score\":65.91,\"quantity\":270,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-443750\",\"name\":\"papa kilo\",\"status\":\"archived\",\"score\":66.03,\"quantity\":312,\"active\":false,\"updatedAt\":\"2026-02-01T12:00:00Z\"}]", + "encoded": "[7]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-588157\",juliet oscar,active,13.52,353,true,\"2026-02-26T12:00:00Z\"\n 1,\"SKU-943313\",kilo oscar,pending,66.62,489,true,\"2026-05-10T12:00:00Z\"\n 2,\"SKU-909215\",mike whiskey,active,15.08,159,true,\"2026-04-28T12:00:00Z\"\n 3,\"SKU-21248\",papa oscar,failed,25.06,481,false,\"2026-06-09T12:00:00Z\"\n 4,\"SKU-512470\",juliet foxtrot,pending,94.43,313,false,\"2026-04-11T12:00:00Z\"\n 5,\"SKU-445960\",echo november,failed,65.91,270,true,\"2026-04-24T12:00:00Z\"\n 6,\"SKU-443750\",papa kilo,archived,66.03,312,false,\"2026-02-01T12:00:00Z\"" + } + }, + { + "name": "bench-1kb-9", + "rawContent": "Tool run 710 output: juliet hotel alpha delta papa golf bravo lima quebec delta romeo foxtrot golf oscar india india oscar lima tango charlie echo india victor uniform whiskey lima echo papa oscar sierra whiskey juliet juliet delta victor november echo alpha uniform charlie mike mike oscar zulu mike quebec mike victor victor charlie foxtrot whiskey juliet quebec mike kilo hotel india india papa uniform victor foxtrot oscar lima mike romeo november juliet zulu foxtrot kilo bravo delta bravo charlie india sierra mike kilo juliet zulu romeo kilo india romeo golf oscar golf charlie uniform juliet charlie whiskey bravo sierra november bravo oscar victor echo uniform foxtrot zulu kilo kilo alpha juliet bravo juliet lima juliet alpha zulu sierra foxtrot charlie echo quebec tango zulu uniform victor foxtrot quebec mike tango foxtrot alpha uniform victor whiskey foxtrot delta alpha whiskey quebec papa charlie papa quebec tango alpha uniform foxtrot mike india bravo golf victor zulu november echo alpha tango alpha whiskey", + "unwrap": true, + "expected": { + "normalized": "Tool run 710 output: juliet hotel alpha delta papa golf bravo lima quebec delta romeo foxtrot golf oscar india india oscar lima tango charlie echo india victor uniform whiskey lima echo papa oscar sierra whiskey juliet juliet delta victor november echo alpha uniform charlie mike mike oscar zulu mike quebec mike victor victor charlie foxtrot whiskey juliet quebec mike kilo hotel india india papa uniform victor foxtrot oscar lima mike romeo november juliet zulu foxtrot kilo bravo delta bravo charlie india sierra mike kilo juliet zulu romeo kilo india romeo golf oscar golf charlie uniform juliet charlie whiskey bravo sierra november bravo oscar victor echo uniform foxtrot zulu kilo kilo alpha juliet bravo juliet lima juliet alpha zulu sierra foxtrot charlie echo quebec tango zulu uniform victor foxtrot quebec mike tango foxtrot alpha uniform victor whiskey foxtrot delta alpha whiskey quebec papa charlie papa quebec tango alpha uniform foxtrot mike india bravo golf victor zulu november echo alpha tango alpha whiskey", + "encoded": null + } + }, + { + "name": "bench-10kb-0", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-11704\",\"name\":\"bravo zulu\",\"status\":\"archived\",\"score\":52.14,\"quantity\":202,\"active\":false,\"updatedAt\":\"2026-02-16T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-729822\",\"name\":\"golf delta\",\"status\":\"failed\",\"score\":51.84,\"quantity\":98,\"active\":false,\"updatedAt\":\"2026-02-15T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-296330\",\"name\":\"zulu foxtrot\",\"status\":\"failed\",\"score\":19.08,\"quantity\":71,\"active\":false,\"updatedAt\":\"2026-02-16T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-761873\",\"name\":\"bravo victor\",\"status\":\"archived\",\"score\":62.16,\"quantity\":340,\"active\":true,\"updatedAt\":\"2026-04-26T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-827628\",\"name\":\"alpha echo\",\"status\":\"pending\",\"score\":42.53,\"quantity\":10,\"active\":false,\"updatedAt\":\"2026-05-11T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-426715\",\"name\":\"lima whiskey\",\"status\":\"archived\",\"score\":68.83,\"quantity\":165,\"active\":false,\"updatedAt\":\"2026-01-04T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-913034\",\"name\":\"hotel echo\",\"status\":\"archived\",\"score\":33.68,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-06-16T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-826577\",\"name\":\"papa whiskey\",\"status\":\"active\",\"score\":52.99,\"quantity\":324,\"active\":true,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":8,\"sku\":\"SKU-594234\",\"name\":\"zulu zulu\",\"status\":\"archived\",\"score\":81.85,\"quantity\":2,\"active\":true,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":9,\"sku\":\"SKU-549495\",\"name\":\"sierra zulu\",\"status\":\"archived\",\"score\":82.32,\"quantity\":214,\"active\":true,\"updatedAt\":\"2026-05-16T12:00:00Z\"},{\"id\":10,\"sku\":\"SKU-312383\",\"name\":\"foxtrot alpha\",\"status\":\"active\",\"score\":94.34,\"quantity\":416,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\"},{\"id\":11,\"sku\":\"SKU-594896\",\"name\":\"romeo zulu\",\"status\":\"pending\",\"score\":98.63,\"quantity\":324,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\"},{\"id\":12,\"sku\":\"SKU-139064\",\"name\":\"zulu mike\",\"status\":\"archived\",\"score\":16.09,\"quantity\":198,\"active\":false,\"updatedAt\":\"2026-05-25T12:00:00Z\"},{\"id\":13,\"sku\":\"SKU-735919\",\"name\":\"papa zulu\",\"status\":\"archived\",\"score\":73.65,\"quantity\":477,\"active\":false,\"updatedAt\":\"2026-02-17T12:00:00Z\"},{\"id\":14,\"sku\":\"SKU-795681\",\"name\":\"uniform echo\",\"status\":\"archived\",\"score\":47.77,\"quantity\":262,\"active\":false,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":15,\"sku\":\"SKU-477512\",\"name\":\"echo alpha\",\"status\":\"active\",\"score\":2.41,\"quantity\":53,\"active\":true,\"updatedAt\":\"2026-06-02T12:00:00Z\"},{\"id\":16,\"sku\":\"SKU-174454\",\"name\":\"juliet lima\",\"status\":\"archived\",\"score\":36.82,\"quantity\":180,\"active\":true,\"updatedAt\":\"2026-05-23T12:00:00Z\"},{\"id\":17,\"sku\":\"SKU-529989\",\"name\":\"tango juliet\",\"status\":\"archived\",\"score\":29.26,\"quantity\":263,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":18,\"sku\":\"SKU-826842\",\"name\":\"tango sierra\",\"status\":\"pending\",\"score\":35.69,\"quantity\":460,\"active\":false,\"updatedAt\":\"2026-03-14T12:00:00Z\"},{\"id\":19,\"sku\":\"SKU-830306\",\"name\":\"juliet zulu\",\"status\":\"archived\",\"score\":43.63,\"quantity\":169,\"active\":true,\"updatedAt\":\"2026-02-25T12:00:00Z\"},{\"id\":20,\"sku\":\"SKU-572258\",\"name\":\"zulu uniform\",\"status\":\"pending\",\"score\":43.68,\"quantity\":348,\"active\":true,\"updatedAt\":\"2026-04-19T12:00:00Z\"},{\"id\":21,\"sku\":\"SKU-162384\",\"name\":\"quebec juliet\",\"status\":\"archived\",\"score\":85.6,\"quantity\":191,\"active\":false,\"updatedAt\":\"2026-01-20T12:00:00Z\"},{\"id\":22,\"sku\":\"SKU-732128\",\"name\":\"zulu hotel\",\"status\":\"pending\",\"score\":6.74,\"quantity\":360,\"active\":true,\"updatedAt\":\"2026-05-24T12:00:00Z\"},{\"id\":23,\"sku\":\"SKU-962738\",\"name\":\"foxtrot zulu\",\"status\":\"pending\",\"score\":53.84,\"quantity\":195,\"active\":true,\"updatedAt\":\"2026-05-18T12:00:00Z\"},{\"id\":24,\"sku\":\"SKU-725523\",\"name\":\"oscar oscar\",\"status\":\"pending\",\"score\":83.19,\"quantity\":241,\"active\":false,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":25,\"sku\":\"SKU-559534\",\"name\":\"november victor\",\"status\":\"failed\",\"score\":15.33,\"quantity\":50,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":26,\"sku\":\"SKU-910057\",\"name\":\"uniform mike\",\"status\":\"failed\",\"score\":27.87,\"quantity\":246,\"active\":true,\"updatedAt\":\"2026-05-23T12:00:00Z\"},{\"id\":27,\"sku\":\"SKU-333534\",\"name\":\"victor zulu\",\"status\":\"failed\",\"score\":0.27,\"quantity\":83,\"active\":true,\"updatedAt\":\"2026-01-03T12:00:00Z\"},{\"id\":28,\"sku\":\"SKU-911823\",\"name\":\"golf oscar\",\"status\":\"pending\",\"score\":79.39,\"quantity\":359,\"active\":false,\"updatedAt\":\"2026-03-28T12:00:00Z\"},{\"id\":29,\"sku\":\"SKU-82374\",\"name\":\"charlie juliet\",\"status\":\"failed\",\"score\":69.13,\"quantity\":135,\"active\":false,\"updatedAt\":\"2026-01-14T12:00:00Z\"},{\"id\":30,\"sku\":\"SKU-437150\",\"name\":\"november echo\",\"status\":\"failed\",\"score\":30.32,\"quantity\":345,\"active\":true,\"updatedAt\":\"2026-01-06T12:00:00Z\"},{\"id\":31,\"sku\":\"SKU-717183\",\"name\":\"victor golf\",\"status\":\"failed\",\"score\":64.44,\"quantity\":161,\"active\":false,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":32,\"sku\":\"SKU-693431\",\"name\":\"golf mike\",\"status\":\"active\",\"score\":51.6,\"quantity\":213,\"active\":true,\"updatedAt\":\"2026-01-21T12:00:00Z\"},{\"id\":33,\"sku\":\"SKU-170458\",\"name\":\"foxtrot echo\",\"status\":\"failed\",\"score\":3.08,\"quantity\":16,\"active\":true,\"updatedAt\":\"2026-06-14T12:00:00Z\"},{\"id\":34,\"sku\":\"SKU-575652\",\"name\":\"quebec echo\",\"status\":\"archived\",\"score\":7.45,\"quantity\":297,\"active\":true,\"updatedAt\":\"2026-06-02T12:00:00Z\"},{\"id\":35,\"sku\":\"SKU-570174\",\"name\":\"india bravo\",\"status\":\"failed\",\"score\":27.28,\"quantity\":77,\"active\":false,\"updatedAt\":\"2026-03-23T12:00:00Z\"},{\"id\":36,\"sku\":\"SKU-823612\",\"name\":\"uniform sierra\",\"status\":\"archived\",\"score\":86.48,\"quantity\":318,\"active\":true,\"updatedAt\":\"2026-05-09T12:00:00Z\"},{\"id\":37,\"sku\":\"SKU-403326\",\"name\":\"papa hotel\",\"status\":\"active\",\"score\":49.65,\"quantity\":299,\"active\":true,\"updatedAt\":\"2026-03-20T12:00:00Z\"},{\"id\":38,\"sku\":\"SKU-241598\",\"name\":\"romeo mike\",\"status\":\"pending\",\"score\":39.17,\"quantity\":407,\"active\":false,\"updatedAt\":\"2026-04-16T12:00:00Z\"},{\"id\":39,\"sku\":\"SKU-659493\",\"name\":\"papa bravo\",\"status\":\"pending\",\"score\":94.52,\"quantity\":356,\"active\":true,\"updatedAt\":\"2026-05-09T12:00:00Z\"},{\"id\":40,\"sku\":\"SKU-975648\",\"name\":\"bravo sierra\",\"status\":\"pending\",\"score\":22.45,\"quantity\":246,\"active\":false,\"updatedAt\":\"2026-06-28T12:00:00Z\"},{\"id\":41,\"sku\":\"SKU-534571\",\"name\":\"whiskey delta\",\"status\":\"pending\",\"score\":17.4,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":42,\"sku\":\"SKU-6562\",\"name\":\"lima papa\",\"status\":\"pending\",\"score\":53.6,\"quantity\":470,\"active\":false,\"updatedAt\":\"2026-04-20T12:00:00Z\"},{\"id\":43,\"sku\":\"SKU-300056\",\"name\":\"charlie papa\",\"status\":\"active\",\"score\":54.68,\"quantity\":226,\"active\":true,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":44,\"sku\":\"SKU-575007\",\"name\":\"sierra mike\",\"status\":\"failed\",\"score\":61.37,\"quantity\":307,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":45,\"sku\":\"SKU-722145\",\"name\":\"victor uniform\",\"status\":\"archived\",\"score\":62.68,\"quantity\":75,\"active\":false,\"updatedAt\":\"2026-06-06T12:00:00Z\"},{\"id\":46,\"sku\":\"SKU-905940\",\"name\":\"alpha romeo\",\"status\":\"archived\",\"score\":82.48,\"quantity\":135,\"active\":false,\"updatedAt\":\"2026-02-20T12:00:00Z\"},{\"id\":47,\"sku\":\"SKU-645839\",\"name\":\"hotel charlie\",\"status\":\"pending\",\"score\":93.84,\"quantity\":47,\"active\":false,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":48,\"sku\":\"SKU-524\",\"name\":\"romeo alpha\",\"status\":\"failed\",\"score\":50.78,\"quantity\":485,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":49,\"sku\":\"SKU-274904\",\"name\":\"india papa\",\"status\":\"failed\",\"score\":53.95,\"quantity\":369,\"active\":false,\"updatedAt\":\"2026-04-07T12:00:00Z\"},{\"id\":50,\"sku\":\"SKU-63590\",\"name\":\"juliet foxtrot\",\"status\":\"failed\",\"score\":16.3,\"quantity\":162,\"active\":false,\"updatedAt\":\"2026-03-27T12:00:00Z\"},{\"id\":51,\"sku\":\"SKU-888095\",\"name\":\"oscar romeo\",\"status\":\"failed\",\"score\":1.55,\"quantity\":98,\"active\":false,\"updatedAt\":\"2026-03-13T12:00:00Z\"},{\"id\":52,\"sku\":\"SKU-152688\",\"name\":\"kilo india\",\"status\":\"active\",\"score\":0.62,\"quantity\":317,\"active\":false,\"updatedAt\":\"2026-02-02T12:00:00Z\"},{\"id\":53,\"sku\":\"SKU-438587\",\"name\":\"sierra uniform\",\"status\":\"archived\",\"score\":71.89,\"quantity\":492,\"active\":true,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":54,\"sku\":\"SKU-816723\",\"name\":\"tango whiskey\",\"status\":\"archived\",\"score\":70.58,\"quantity\":160,\"active\":false,\"updatedAt\":\"2026-05-10T12:00:00Z\"},{\"id\":55,\"sku\":\"SKU-762042\",\"name\":\"bravo november\",\"status\":\"archived\",\"score\":23.99,\"quantity\":229,\"active\":true,\"updatedAt\":\"2026-06-09T12:00:00Z\"},{\"id\":56,\"sku\":\"SKU-14029\",\"name\":\"november lima\",\"status\":\"active\",\"score\":5.02,\"quantity\":201,\"active\":false,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":57,\"sku\":\"SKU-798172\",\"name\":\"sierra sierra\",\"status\":\"archived\",\"score\":92.92,\"quantity\":455,\"active\":true,\"updatedAt\":\"2026-06-26T12:00:00Z\"},{\"id\":58,\"sku\":\"SKU-338632\",\"name\":\"victor tango\",\"status\":\"pending\",\"score\":39.96,\"quantity\":140,\"active\":false,\"updatedAt\":\"2026-06-21T12:00:00Z\"},{\"id\":59,\"sku\":\"SKU-344716\",\"name\":\"sierra romeo\",\"status\":\"archived\",\"score\":72.13,\"quantity\":310,\"active\":true,\"updatedAt\":\"2026-01-23T12:00:00Z\"},{\"id\":60,\"sku\":\"SKU-917943\",\"name\":\"echo juliet\",\"status\":\"archived\",\"score\":77.74,\"quantity\":255,\"active\":false,\"updatedAt\":\"2026-06-28T12:00:00Z\"},{\"id\":61,\"sku\":\"SKU-150627\",\"name\":\"papa foxtrot\",\"status\":\"archived\",\"score\":47.01,\"quantity\":272,\"active\":false,\"updatedAt\":\"2026-05-17T12:00:00Z\"},{\"id\":62,\"sku\":\"SKU-838601\",\"name\":\"charlie echo\",\"status\":\"failed\",\"score\":7.23,\"quantity\":408,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\"},{\"id\":63,\"sku\":\"SKU-809961\",\"name\":\"foxtrot delta\",\"status\":\"pending\",\"score\":60.76,\"quantity\":51,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\"},{\"id\":64,\"sku\":\"SKU-513516\",\"name\":\"echo tango\",\"status\":\"failed\",\"score\":67.25,\"quantity\":16,\"active\":false,\"updatedAt\":\"2026-04-07T12:00:00Z\"},{\"id\":65,\"sku\":\"SKU-304984\",\"name\":\"kilo echo\",\"status\":\"active\",\"score\":47.84,\"quantity\":344,\"active\":true,\"updatedAt\":\"2026-04-19T12:00:00Z\"},{\"id\":66,\"sku\":\"SKU-469514\",\"name\":\"india charlie\",\"status\":\"pending\",\"score\":57.19,\"quantity\":309,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":67,\"sku\":\"SKU-71481\",\"name\":\"lima romeo\",\"status\":\"failed\",\"score\":8.23,\"quantity\":384,\"active\":true,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":68,\"sku\":\"SKU-836554\",\"name\":\"lima bravo\",\"status\":\"active\",\"score\":80.91,\"quantity\":72,\"active\":false,\"updatedAt\":\"2026-05-19T12:00:00Z\"},{\"id\":69,\"sku\":\"SKU-410694\",\"name\":\"mike sierra\",\"status\":\"archived\",\"score\":66.44,\"quantity\":144,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-11704\",\"name\":\"bravo zulu\",\"status\":\"archived\",\"score\":52.14,\"quantity\":202,\"active\":false,\"updatedAt\":\"2026-02-16T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-729822\",\"name\":\"golf delta\",\"status\":\"failed\",\"score\":51.84,\"quantity\":98,\"active\":false,\"updatedAt\":\"2026-02-15T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-296330\",\"name\":\"zulu foxtrot\",\"status\":\"failed\",\"score\":19.08,\"quantity\":71,\"active\":false,\"updatedAt\":\"2026-02-16T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-761873\",\"name\":\"bravo victor\",\"status\":\"archived\",\"score\":62.16,\"quantity\":340,\"active\":true,\"updatedAt\":\"2026-04-26T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-827628\",\"name\":\"alpha echo\",\"status\":\"pending\",\"score\":42.53,\"quantity\":10,\"active\":false,\"updatedAt\":\"2026-05-11T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-426715\",\"name\":\"lima whiskey\",\"status\":\"archived\",\"score\":68.83,\"quantity\":165,\"active\":false,\"updatedAt\":\"2026-01-04T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-913034\",\"name\":\"hotel echo\",\"status\":\"archived\",\"score\":33.68,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-06-16T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-826577\",\"name\":\"papa whiskey\",\"status\":\"active\",\"score\":52.99,\"quantity\":324,\"active\":true,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":8,\"sku\":\"SKU-594234\",\"name\":\"zulu zulu\",\"status\":\"archived\",\"score\":81.85,\"quantity\":2,\"active\":true,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":9,\"sku\":\"SKU-549495\",\"name\":\"sierra zulu\",\"status\":\"archived\",\"score\":82.32,\"quantity\":214,\"active\":true,\"updatedAt\":\"2026-05-16T12:00:00Z\"},{\"id\":10,\"sku\":\"SKU-312383\",\"name\":\"foxtrot alpha\",\"status\":\"active\",\"score\":94.34,\"quantity\":416,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\"},{\"id\":11,\"sku\":\"SKU-594896\",\"name\":\"romeo zulu\",\"status\":\"pending\",\"score\":98.63,\"quantity\":324,\"active\":true,\"updatedAt\":\"2026-04-24T12:00:00Z\"},{\"id\":12,\"sku\":\"SKU-139064\",\"name\":\"zulu mike\",\"status\":\"archived\",\"score\":16.09,\"quantity\":198,\"active\":false,\"updatedAt\":\"2026-05-25T12:00:00Z\"},{\"id\":13,\"sku\":\"SKU-735919\",\"name\":\"papa zulu\",\"status\":\"archived\",\"score\":73.65,\"quantity\":477,\"active\":false,\"updatedAt\":\"2026-02-17T12:00:00Z\"},{\"id\":14,\"sku\":\"SKU-795681\",\"name\":\"uniform echo\",\"status\":\"archived\",\"score\":47.77,\"quantity\":262,\"active\":false,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":15,\"sku\":\"SKU-477512\",\"name\":\"echo alpha\",\"status\":\"active\",\"score\":2.41,\"quantity\":53,\"active\":true,\"updatedAt\":\"2026-06-02T12:00:00Z\"},{\"id\":16,\"sku\":\"SKU-174454\",\"name\":\"juliet lima\",\"status\":\"archived\",\"score\":36.82,\"quantity\":180,\"active\":true,\"updatedAt\":\"2026-05-23T12:00:00Z\"},{\"id\":17,\"sku\":\"SKU-529989\",\"name\":\"tango juliet\",\"status\":\"archived\",\"score\":29.26,\"quantity\":263,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":18,\"sku\":\"SKU-826842\",\"name\":\"tango sierra\",\"status\":\"pending\",\"score\":35.69,\"quantity\":460,\"active\":false,\"updatedAt\":\"2026-03-14T12:00:00Z\"},{\"id\":19,\"sku\":\"SKU-830306\",\"name\":\"juliet zulu\",\"status\":\"archived\",\"score\":43.63,\"quantity\":169,\"active\":true,\"updatedAt\":\"2026-02-25T12:00:00Z\"},{\"id\":20,\"sku\":\"SKU-572258\",\"name\":\"zulu uniform\",\"status\":\"pending\",\"score\":43.68,\"quantity\":348,\"active\":true,\"updatedAt\":\"2026-04-19T12:00:00Z\"},{\"id\":21,\"sku\":\"SKU-162384\",\"name\":\"quebec juliet\",\"status\":\"archived\",\"score\":85.6,\"quantity\":191,\"active\":false,\"updatedAt\":\"2026-01-20T12:00:00Z\"},{\"id\":22,\"sku\":\"SKU-732128\",\"name\":\"zulu hotel\",\"status\":\"pending\",\"score\":6.74,\"quantity\":360,\"active\":true,\"updatedAt\":\"2026-05-24T12:00:00Z\"},{\"id\":23,\"sku\":\"SKU-962738\",\"name\":\"foxtrot zulu\",\"status\":\"pending\",\"score\":53.84,\"quantity\":195,\"active\":true,\"updatedAt\":\"2026-05-18T12:00:00Z\"},{\"id\":24,\"sku\":\"SKU-725523\",\"name\":\"oscar oscar\",\"status\":\"pending\",\"score\":83.19,\"quantity\":241,\"active\":false,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":25,\"sku\":\"SKU-559534\",\"name\":\"november victor\",\"status\":\"failed\",\"score\":15.33,\"quantity\":50,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":26,\"sku\":\"SKU-910057\",\"name\":\"uniform mike\",\"status\":\"failed\",\"score\":27.87,\"quantity\":246,\"active\":true,\"updatedAt\":\"2026-05-23T12:00:00Z\"},{\"id\":27,\"sku\":\"SKU-333534\",\"name\":\"victor zulu\",\"status\":\"failed\",\"score\":0.27,\"quantity\":83,\"active\":true,\"updatedAt\":\"2026-01-03T12:00:00Z\"},{\"id\":28,\"sku\":\"SKU-911823\",\"name\":\"golf oscar\",\"status\":\"pending\",\"score\":79.39,\"quantity\":359,\"active\":false,\"updatedAt\":\"2026-03-28T12:00:00Z\"},{\"id\":29,\"sku\":\"SKU-82374\",\"name\":\"charlie juliet\",\"status\":\"failed\",\"score\":69.13,\"quantity\":135,\"active\":false,\"updatedAt\":\"2026-01-14T12:00:00Z\"},{\"id\":30,\"sku\":\"SKU-437150\",\"name\":\"november echo\",\"status\":\"failed\",\"score\":30.32,\"quantity\":345,\"active\":true,\"updatedAt\":\"2026-01-06T12:00:00Z\"},{\"id\":31,\"sku\":\"SKU-717183\",\"name\":\"victor golf\",\"status\":\"failed\",\"score\":64.44,\"quantity\":161,\"active\":false,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":32,\"sku\":\"SKU-693431\",\"name\":\"golf mike\",\"status\":\"active\",\"score\":51.6,\"quantity\":213,\"active\":true,\"updatedAt\":\"2026-01-21T12:00:00Z\"},{\"id\":33,\"sku\":\"SKU-170458\",\"name\":\"foxtrot echo\",\"status\":\"failed\",\"score\":3.08,\"quantity\":16,\"active\":true,\"updatedAt\":\"2026-06-14T12:00:00Z\"},{\"id\":34,\"sku\":\"SKU-575652\",\"name\":\"quebec echo\",\"status\":\"archived\",\"score\":7.45,\"quantity\":297,\"active\":true,\"updatedAt\":\"2026-06-02T12:00:00Z\"},{\"id\":35,\"sku\":\"SKU-570174\",\"name\":\"india bravo\",\"status\":\"failed\",\"score\":27.28,\"quantity\":77,\"active\":false,\"updatedAt\":\"2026-03-23T12:00:00Z\"},{\"id\":36,\"sku\":\"SKU-823612\",\"name\":\"uniform sierra\",\"status\":\"archived\",\"score\":86.48,\"quantity\":318,\"active\":true,\"updatedAt\":\"2026-05-09T12:00:00Z\"},{\"id\":37,\"sku\":\"SKU-403326\",\"name\":\"papa hotel\",\"status\":\"active\",\"score\":49.65,\"quantity\":299,\"active\":true,\"updatedAt\":\"2026-03-20T12:00:00Z\"},{\"id\":38,\"sku\":\"SKU-241598\",\"name\":\"romeo mike\",\"status\":\"pending\",\"score\":39.17,\"quantity\":407,\"active\":false,\"updatedAt\":\"2026-04-16T12:00:00Z\"},{\"id\":39,\"sku\":\"SKU-659493\",\"name\":\"papa bravo\",\"status\":\"pending\",\"score\":94.52,\"quantity\":356,\"active\":true,\"updatedAt\":\"2026-05-09T12:00:00Z\"},{\"id\":40,\"sku\":\"SKU-975648\",\"name\":\"bravo sierra\",\"status\":\"pending\",\"score\":22.45,\"quantity\":246,\"active\":false,\"updatedAt\":\"2026-06-28T12:00:00Z\"},{\"id\":41,\"sku\":\"SKU-534571\",\"name\":\"whiskey delta\",\"status\":\"pending\",\"score\":17.4,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":42,\"sku\":\"SKU-6562\",\"name\":\"lima papa\",\"status\":\"pending\",\"score\":53.6,\"quantity\":470,\"active\":false,\"updatedAt\":\"2026-04-20T12:00:00Z\"},{\"id\":43,\"sku\":\"SKU-300056\",\"name\":\"charlie papa\",\"status\":\"active\",\"score\":54.68,\"quantity\":226,\"active\":true,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":44,\"sku\":\"SKU-575007\",\"name\":\"sierra mike\",\"status\":\"failed\",\"score\":61.37,\"quantity\":307,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":45,\"sku\":\"SKU-722145\",\"name\":\"victor uniform\",\"status\":\"archived\",\"score\":62.68,\"quantity\":75,\"active\":false,\"updatedAt\":\"2026-06-06T12:00:00Z\"},{\"id\":46,\"sku\":\"SKU-905940\",\"name\":\"alpha romeo\",\"status\":\"archived\",\"score\":82.48,\"quantity\":135,\"active\":false,\"updatedAt\":\"2026-02-20T12:00:00Z\"},{\"id\":47,\"sku\":\"SKU-645839\",\"name\":\"hotel charlie\",\"status\":\"pending\",\"score\":93.84,\"quantity\":47,\"active\":false,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":48,\"sku\":\"SKU-524\",\"name\":\"romeo alpha\",\"status\":\"failed\",\"score\":50.78,\"quantity\":485,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":49,\"sku\":\"SKU-274904\",\"name\":\"india papa\",\"status\":\"failed\",\"score\":53.95,\"quantity\":369,\"active\":false,\"updatedAt\":\"2026-04-07T12:00:00Z\"},{\"id\":50,\"sku\":\"SKU-63590\",\"name\":\"juliet foxtrot\",\"status\":\"failed\",\"score\":16.3,\"quantity\":162,\"active\":false,\"updatedAt\":\"2026-03-27T12:00:00Z\"},{\"id\":51,\"sku\":\"SKU-888095\",\"name\":\"oscar romeo\",\"status\":\"failed\",\"score\":1.55,\"quantity\":98,\"active\":false,\"updatedAt\":\"2026-03-13T12:00:00Z\"},{\"id\":52,\"sku\":\"SKU-152688\",\"name\":\"kilo india\",\"status\":\"active\",\"score\":0.62,\"quantity\":317,\"active\":false,\"updatedAt\":\"2026-02-02T12:00:00Z\"},{\"id\":53,\"sku\":\"SKU-438587\",\"name\":\"sierra uniform\",\"status\":\"archived\",\"score\":71.89,\"quantity\":492,\"active\":true,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":54,\"sku\":\"SKU-816723\",\"name\":\"tango whiskey\",\"status\":\"archived\",\"score\":70.58,\"quantity\":160,\"active\":false,\"updatedAt\":\"2026-05-10T12:00:00Z\"},{\"id\":55,\"sku\":\"SKU-762042\",\"name\":\"bravo november\",\"status\":\"archived\",\"score\":23.99,\"quantity\":229,\"active\":true,\"updatedAt\":\"2026-06-09T12:00:00Z\"},{\"id\":56,\"sku\":\"SKU-14029\",\"name\":\"november lima\",\"status\":\"active\",\"score\":5.02,\"quantity\":201,\"active\":false,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":57,\"sku\":\"SKU-798172\",\"name\":\"sierra sierra\",\"status\":\"archived\",\"score\":92.92,\"quantity\":455,\"active\":true,\"updatedAt\":\"2026-06-26T12:00:00Z\"},{\"id\":58,\"sku\":\"SKU-338632\",\"name\":\"victor tango\",\"status\":\"pending\",\"score\":39.96,\"quantity\":140,\"active\":false,\"updatedAt\":\"2026-06-21T12:00:00Z\"},{\"id\":59,\"sku\":\"SKU-344716\",\"name\":\"sierra romeo\",\"status\":\"archived\",\"score\":72.13,\"quantity\":310,\"active\":true,\"updatedAt\":\"2026-01-23T12:00:00Z\"},{\"id\":60,\"sku\":\"SKU-917943\",\"name\":\"echo juliet\",\"status\":\"archived\",\"score\":77.74,\"quantity\":255,\"active\":false,\"updatedAt\":\"2026-06-28T12:00:00Z\"},{\"id\":61,\"sku\":\"SKU-150627\",\"name\":\"papa foxtrot\",\"status\":\"archived\",\"score\":47.01,\"quantity\":272,\"active\":false,\"updatedAt\":\"2026-05-17T12:00:00Z\"},{\"id\":62,\"sku\":\"SKU-838601\",\"name\":\"charlie echo\",\"status\":\"failed\",\"score\":7.23,\"quantity\":408,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\"},{\"id\":63,\"sku\":\"SKU-809961\",\"name\":\"foxtrot delta\",\"status\":\"pending\",\"score\":60.76,\"quantity\":51,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\"},{\"id\":64,\"sku\":\"SKU-513516\",\"name\":\"echo tango\",\"status\":\"failed\",\"score\":67.25,\"quantity\":16,\"active\":false,\"updatedAt\":\"2026-04-07T12:00:00Z\"},{\"id\":65,\"sku\":\"SKU-304984\",\"name\":\"kilo echo\",\"status\":\"active\",\"score\":47.84,\"quantity\":344,\"active\":true,\"updatedAt\":\"2026-04-19T12:00:00Z\"},{\"id\":66,\"sku\":\"SKU-469514\",\"name\":\"india charlie\",\"status\":\"pending\",\"score\":57.19,\"quantity\":309,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":67,\"sku\":\"SKU-71481\",\"name\":\"lima romeo\",\"status\":\"failed\",\"score\":8.23,\"quantity\":384,\"active\":true,\"updatedAt\":\"2026-03-25T12:00:00Z\"},{\"id\":68,\"sku\":\"SKU-836554\",\"name\":\"lima bravo\",\"status\":\"active\",\"score\":80.91,\"quantity\":72,\"active\":false,\"updatedAt\":\"2026-05-19T12:00:00Z\"},{\"id\":69,\"sku\":\"SKU-410694\",\"name\":\"mike sierra\",\"status\":\"archived\",\"score\":66.44,\"quantity\":144,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"}]", + "encoded": "[70]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-11704\",bravo zulu,archived,52.14,202,false,\"2026-02-16T12:00:00Z\"\n 1,\"SKU-729822\",golf delta,failed,51.84,98,false,\"2026-02-15T12:00:00Z\"\n 2,\"SKU-296330\",zulu foxtrot,failed,19.08,71,false,\"2026-02-16T12:00:00Z\"\n 3,\"SKU-761873\",bravo victor,archived,62.16,340,true,\"2026-04-26T12:00:00Z\"\n 4,\"SKU-827628\",alpha echo,pending,42.53,10,false,\"2026-05-11T12:00:00Z\"\n 5,\"SKU-426715\",lima whiskey,archived,68.83,165,false,\"2026-01-04T12:00:00Z\"\n 6,\"SKU-913034\",hotel echo,archived,33.68,21,true,\"2026-06-16T12:00:00Z\"\n 7,\"SKU-826577\",papa whiskey,active,52.99,324,true,\"2026-06-24T12:00:00Z\"\n 8,\"SKU-594234\",zulu zulu,archived,81.85,2,true,\"2026-02-21T12:00:00Z\"\n 9,\"SKU-549495\",sierra zulu,archived,82.32,214,true,\"2026-05-16T12:00:00Z\"\n 10,\"SKU-312383\",foxtrot alpha,active,94.34,416,true,\"2026-01-18T12:00:00Z\"\n 11,\"SKU-594896\",romeo zulu,pending,98.63,324,true,\"2026-04-24T12:00:00Z\"\n 12,\"SKU-139064\",zulu mike,archived,16.09,198,false,\"2026-05-25T12:00:00Z\"\n 13,\"SKU-735919\",papa zulu,archived,73.65,477,false,\"2026-02-17T12:00:00Z\"\n 14,\"SKU-795681\",uniform echo,archived,47.77,262,false,\"2026-02-13T12:00:00Z\"\n 15,\"SKU-477512\",echo alpha,active,2.41,53,true,\"2026-06-02T12:00:00Z\"\n 16,\"SKU-174454\",juliet lima,archived,36.82,180,true,\"2026-05-23T12:00:00Z\"\n 17,\"SKU-529989\",tango juliet,archived,29.26,263,true,\"2026-01-08T12:00:00Z\"\n 18,\"SKU-826842\",tango sierra,pending,35.69,460,false,\"2026-03-14T12:00:00Z\"\n 19,\"SKU-830306\",juliet zulu,archived,43.63,169,true,\"2026-02-25T12:00:00Z\"\n 20,\"SKU-572258\",zulu uniform,pending,43.68,348,true,\"2026-04-19T12:00:00Z\"\n 21,\"SKU-162384\",quebec juliet,archived,85.6,191,false,\"2026-01-20T12:00:00Z\"\n 22,\"SKU-732128\",zulu hotel,pending,6.74,360,true,\"2026-05-24T12:00:00Z\"\n 23,\"SKU-962738\",foxtrot zulu,pending,53.84,195,true,\"2026-05-18T12:00:00Z\"\n 24,\"SKU-725523\",oscar oscar,pending,83.19,241,false,\"2026-05-13T12:00:00Z\"\n 25,\"SKU-559534\",november victor,failed,15.33,50,false,\"2026-04-11T12:00:00Z\"\n 26,\"SKU-910057\",uniform mike,failed,27.87,246,true,\"2026-05-23T12:00:00Z\"\n 27,\"SKU-333534\",victor zulu,failed,0.27,83,true,\"2026-01-03T12:00:00Z\"\n 28,\"SKU-911823\",golf oscar,pending,79.39,359,false,\"2026-03-28T12:00:00Z\"\n 29,\"SKU-82374\",charlie juliet,failed,69.13,135,false,\"2026-01-14T12:00:00Z\"\n 30,\"SKU-437150\",november echo,failed,30.32,345,true,\"2026-01-06T12:00:00Z\"\n 31,\"SKU-717183\",victor golf,failed,64.44,161,false,\"2026-01-28T12:00:00Z\"\n 32,\"SKU-693431\",golf mike,active,51.6,213,true,\"2026-01-21T12:00:00Z\"\n 33,\"SKU-170458\",foxtrot echo,failed,3.08,16,true,\"2026-06-14T12:00:00Z\"\n 34,\"SKU-575652\",quebec echo,archived,7.45,297,true,\"2026-06-02T12:00:00Z\"\n 35,\"SKU-570174\",india bravo,failed,27.28,77,false,\"2026-03-23T12:00:00Z\"\n 36,\"SKU-823612\",uniform sierra,archived,86.48,318,true,\"2026-05-09T12:00:00Z\"\n 37,\"SKU-403326\",papa hotel,active,49.65,299,true,\"2026-03-20T12:00:00Z\"\n 38,\"SKU-241598\",romeo mike,pending,39.17,407,false,\"2026-04-16T12:00:00Z\"\n 39,\"SKU-659493\",papa bravo,pending,94.52,356,true,\"2026-05-09T12:00:00Z\"\n 40,\"SKU-975648\",bravo sierra,pending,22.45,246,false,\"2026-06-28T12:00:00Z\"\n 41,\"SKU-534571\",whiskey delta,pending,17.4,21,true,\"2026-05-07T12:00:00Z\"\n 42,\"SKU-6562\",lima papa,pending,53.6,470,false,\"2026-04-20T12:00:00Z\"\n 43,\"SKU-300056\",charlie papa,active,54.68,226,true,\"2026-05-21T12:00:00Z\"\n 44,\"SKU-575007\",sierra mike,failed,61.37,307,true,\"2026-05-22T12:00:00Z\"\n 45,\"SKU-722145\",victor uniform,archived,62.68,75,false,\"2026-06-06T12:00:00Z\"\n 46,\"SKU-905940\",alpha romeo,archived,82.48,135,false,\"2026-02-20T12:00:00Z\"\n 47,\"SKU-645839\",hotel charlie,pending,93.84,47,false,\"2026-02-21T12:00:00Z\"\n 48,\"SKU-524\",romeo alpha,failed,50.78,485,true,\"2026-03-22T12:00:00Z\"\n 49,\"SKU-274904\",india papa,failed,53.95,369,false,\"2026-04-07T12:00:00Z\"\n 50,\"SKU-63590\",juliet foxtrot,failed,16.3,162,false,\"2026-03-27T12:00:00Z\"\n 51,\"SKU-888095\",oscar romeo,failed,1.55,98,false,\"2026-03-13T12:00:00Z\"\n 52,\"SKU-152688\",kilo india,active,0.62,317,false,\"2026-02-02T12:00:00Z\"\n 53,\"SKU-438587\",sierra uniform,archived,71.89,492,true,\"2026-03-25T12:00:00Z\"\n 54,\"SKU-816723\",tango whiskey,archived,70.58,160,false,\"2026-05-10T12:00:00Z\"\n 55,\"SKU-762042\",bravo november,archived,23.99,229,true,\"2026-06-09T12:00:00Z\"\n 56,\"SKU-14029\",november lima,active,5.02,201,false,\"2026-05-21T12:00:00Z\"\n 57,\"SKU-798172\",sierra sierra,archived,92.92,455,true,\"2026-06-26T12:00:00Z\"\n 58,\"SKU-338632\",victor tango,pending,39.96,140,false,\"2026-06-21T12:00:00Z\"\n 59,\"SKU-344716\",sierra romeo,archived,72.13,310,true,\"2026-01-23T12:00:00Z\"\n 60,\"SKU-917943\",echo juliet,archived,77.74,255,false,\"2026-06-28T12:00:00Z\"\n 61,\"SKU-150627\",papa foxtrot,archived,47.01,272,false,\"2026-05-17T12:00:00Z\"\n 62,\"SKU-838601\",charlie echo,failed,7.23,408,false,\"2026-04-10T12:00:00Z\"\n 63,\"SKU-809961\",foxtrot delta,pending,60.76,51,true,\"2026-01-18T12:00:00Z\"\n 64,\"SKU-513516\",echo tango,failed,67.25,16,false,\"2026-04-07T12:00:00Z\"\n 65,\"SKU-304984\",kilo echo,active,47.84,344,true,\"2026-04-19T12:00:00Z\"\n 66,\"SKU-469514\",india charlie,pending,57.19,309,true,\"2026-01-08T12:00:00Z\"\n 67,\"SKU-71481\",lima romeo,failed,8.23,384,true,\"2026-03-25T12:00:00Z\"\n 68,\"SKU-836554\",lima bravo,active,80.91,72,false,\"2026-05-19T12:00:00Z\"\n 69,\"SKU-410694\",mike sierra,archived,66.44,144,false,\"2026-06-24T12:00:00Z\"" + } + }, + { + "name": "bench-10kb-1", + "rawContent": "[{\"id\":0,\"sku\":\"SKU-369981\",\"name\":\"lima oscar\",\"status\":\"failed\",\"score\":9.74,\"quantity\":27,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-967340\",\"name\":\"sierra kilo\",\"status\":\"pending\",\"score\":37.31,\"quantity\":46,\"active\":false,\"updatedAt\":\"2026-02-04T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-334172\",\"name\":\"mike golf\",\"status\":\"archived\",\"score\":94.56,\"quantity\":499,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-708572\",\"name\":\"uniform kilo\",\"status\":\"failed\",\"score\":91.72,\"quantity\":414,\"active\":false,\"updatedAt\":\"2026-01-12T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-59452\",\"name\":\"alpha sierra\",\"status\":\"active\",\"score\":21.56,\"quantity\":215,\"active\":false,\"updatedAt\":\"2026-06-20T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-354103\",\"name\":\"zulu hotel\",\"status\":\"pending\",\"score\":59.22,\"quantity\":250,\"active\":true,\"updatedAt\":\"2026-05-02T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-868744\",\"name\":\"whiskey tango\",\"status\":\"active\",\"score\":46.31,\"quantity\":111,\"active\":true,\"updatedAt\":\"2026-01-05T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-834024\",\"name\":\"delta delta\",\"status\":\"archived\",\"score\":88.68,\"quantity\":221,\"active\":true,\"updatedAt\":\"2026-06-10T12:00:00Z\"},{\"id\":8,\"sku\":\"SKU-4318\",\"name\":\"alpha lima\",\"status\":\"failed\",\"score\":86.59,\"quantity\":237,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":9,\"sku\":\"SKU-397052\",\"name\":\"bravo quebec\",\"status\":\"active\",\"score\":14.17,\"quantity\":244,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":10,\"sku\":\"SKU-987401\",\"name\":\"november sierra\",\"status\":\"failed\",\"score\":67.5,\"quantity\":212,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":11,\"sku\":\"SKU-399261\",\"name\":\"charlie tango\",\"status\":\"active\",\"score\":12.8,\"quantity\":104,\"active\":false,\"updatedAt\":\"2026-05-01T12:00:00Z\"},{\"id\":12,\"sku\":\"SKU-541132\",\"name\":\"india november\",\"status\":\"active\",\"score\":8.18,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-02-08T12:00:00Z\"},{\"id\":13,\"sku\":\"SKU-664917\",\"name\":\"november golf\",\"status\":\"pending\",\"score\":39.06,\"quantity\":87,\"active\":true,\"updatedAt\":\"2026-02-22T12:00:00Z\"},{\"id\":14,\"sku\":\"SKU-470178\",\"name\":\"papa uniform\",\"status\":\"failed\",\"score\":31.51,\"quantity\":379,\"active\":true,\"updatedAt\":\"2026-04-28T12:00:00Z\"},{\"id\":15,\"sku\":\"SKU-795808\",\"name\":\"papa india\",\"status\":\"active\",\"score\":81.8,\"quantity\":443,\"active\":true,\"updatedAt\":\"2026-02-17T12:00:00Z\"},{\"id\":16,\"sku\":\"SKU-501650\",\"name\":\"golf india\",\"status\":\"pending\",\"score\":94.3,\"quantity\":462,\"active\":true,\"updatedAt\":\"2026-05-11T12:00:00Z\"},{\"id\":17,\"sku\":\"SKU-562629\",\"name\":\"charlie echo\",\"status\":\"archived\",\"score\":34.87,\"quantity\":280,\"active\":true,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":18,\"sku\":\"SKU-32189\",\"name\":\"zulu mike\",\"status\":\"pending\",\"score\":58.38,\"quantity\":372,\"active\":false,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":19,\"sku\":\"SKU-197718\",\"name\":\"india kilo\",\"status\":\"active\",\"score\":19.81,\"quantity\":296,\"active\":true,\"updatedAt\":\"2026-03-03T12:00:00Z\"},{\"id\":20,\"sku\":\"SKU-770813\",\"name\":\"whiskey delta\",\"status\":\"archived\",\"score\":14.07,\"quantity\":375,\"active\":false,\"updatedAt\":\"2026-06-11T12:00:00Z\"},{\"id\":21,\"sku\":\"SKU-993297\",\"name\":\"lima oscar\",\"status\":\"pending\",\"score\":49.46,\"quantity\":320,\"active\":true,\"updatedAt\":\"2026-06-04T12:00:00Z\"},{\"id\":22,\"sku\":\"SKU-129005\",\"name\":\"india delta\",\"status\":\"active\",\"score\":38.89,\"quantity\":1,\"active\":true,\"updatedAt\":\"2026-06-18T12:00:00Z\"},{\"id\":23,\"sku\":\"SKU-983730\",\"name\":\"bravo papa\",\"status\":\"pending\",\"score\":88.65,\"quantity\":367,\"active\":false,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":24,\"sku\":\"SKU-758767\",\"name\":\"romeo tango\",\"status\":\"pending\",\"score\":92.43,\"quantity\":327,\"active\":false,\"updatedAt\":\"2026-04-21T12:00:00Z\"},{\"id\":25,\"sku\":\"SKU-642530\",\"name\":\"charlie whiskey\",\"status\":\"failed\",\"score\":91.16,\"quantity\":139,\"active\":true,\"updatedAt\":\"2026-06-13T12:00:00Z\"},{\"id\":26,\"sku\":\"SKU-465260\",\"name\":\"kilo lima\",\"status\":\"archived\",\"score\":35.1,\"quantity\":257,\"active\":true,\"updatedAt\":\"2026-01-16T12:00:00Z\"},{\"id\":27,\"sku\":\"SKU-73648\",\"name\":\"india mike\",\"status\":\"archived\",\"score\":97.62,\"quantity\":386,\"active\":true,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":28,\"sku\":\"SKU-776786\",\"name\":\"victor lima\",\"status\":\"active\",\"score\":27.69,\"quantity\":438,\"active\":true,\"updatedAt\":\"2026-05-27T12:00:00Z\"},{\"id\":29,\"sku\":\"SKU-468983\",\"name\":\"hotel kilo\",\"status\":\"archived\",\"score\":0.48,\"quantity\":239,\"active\":true,\"updatedAt\":\"2026-02-09T12:00:00Z\"},{\"id\":30,\"sku\":\"SKU-563299\",\"name\":\"charlie november\",\"status\":\"archived\",\"score\":13.56,\"quantity\":88,\"active\":true,\"updatedAt\":\"2026-05-08T12:00:00Z\"},{\"id\":31,\"sku\":\"SKU-419442\",\"name\":\"romeo charlie\",\"status\":\"pending\",\"score\":26.86,\"quantity\":346,\"active\":true,\"updatedAt\":\"2026-04-15T12:00:00Z\"},{\"id\":32,\"sku\":\"SKU-182808\",\"name\":\"november tango\",\"status\":\"pending\",\"score\":95.74,\"quantity\":288,\"active\":false,\"updatedAt\":\"2026-04-25T12:00:00Z\"},{\"id\":33,\"sku\":\"SKU-921862\",\"name\":\"zulu sierra\",\"status\":\"active\",\"score\":38.59,\"quantity\":370,\"active\":true,\"updatedAt\":\"2026-02-02T12:00:00Z\"},{\"id\":34,\"sku\":\"SKU-809989\",\"name\":\"golf echo\",\"status\":\"archived\",\"score\":13.09,\"quantity\":427,\"active\":false,\"updatedAt\":\"2026-04-23T12:00:00Z\"},{\"id\":35,\"sku\":\"SKU-684809\",\"name\":\"romeo foxtrot\",\"status\":\"pending\",\"score\":82.4,\"quantity\":444,\"active\":false,\"updatedAt\":\"2026-03-19T12:00:00Z\"},{\"id\":36,\"sku\":\"SKU-56758\",\"name\":\"november victor\",\"status\":\"pending\",\"score\":57.61,\"quantity\":122,\"active\":true,\"updatedAt\":\"2026-02-06T12:00:00Z\"},{\"id\":37,\"sku\":\"SKU-158771\",\"name\":\"hotel quebec\",\"status\":\"archived\",\"score\":52.21,\"quantity\":108,\"active\":true,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":38,\"sku\":\"SKU-201657\",\"name\":\"india mike\",\"status\":\"archived\",\"score\":11.98,\"quantity\":279,\"active\":true,\"updatedAt\":\"2026-04-17T12:00:00Z\"},{\"id\":39,\"sku\":\"SKU-934265\",\"name\":\"oscar victor\",\"status\":\"failed\",\"score\":10.39,\"quantity\":10,\"active\":true,\"updatedAt\":\"2026-04-09T12:00:00Z\"},{\"id\":40,\"sku\":\"SKU-980470\",\"name\":\"delta zulu\",\"status\":\"failed\",\"score\":95.34,\"quantity\":73,\"active\":false,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":41,\"sku\":\"SKU-522215\",\"name\":\"november lima\",\"status\":\"archived\",\"score\":64.84,\"quantity\":369,\"active\":true,\"updatedAt\":\"2026-05-16T12:00:00Z\"},{\"id\":42,\"sku\":\"SKU-409943\",\"name\":\"hotel zulu\",\"status\":\"archived\",\"score\":11.64,\"quantity\":426,\"active\":true,\"updatedAt\":\"2026-02-11T12:00:00Z\"},{\"id\":43,\"sku\":\"SKU-77806\",\"name\":\"mike lima\",\"status\":\"active\",\"score\":63.83,\"quantity\":203,\"active\":true,\"updatedAt\":\"2026-03-19T12:00:00Z\"},{\"id\":44,\"sku\":\"SKU-928201\",\"name\":\"charlie mike\",\"status\":\"pending\",\"score\":5.06,\"quantity\":450,\"active\":false,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":45,\"sku\":\"SKU-619926\",\"name\":\"bravo whiskey\",\"status\":\"pending\",\"score\":20.01,\"quantity\":155,\"active\":true,\"updatedAt\":\"2026-02-06T12:00:00Z\"},{\"id\":46,\"sku\":\"SKU-22057\",\"name\":\"charlie sierra\",\"status\":\"failed\",\"score\":33.47,\"quantity\":435,\"active\":true,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":47,\"sku\":\"SKU-638993\",\"name\":\"alpha charlie\",\"status\":\"archived\",\"score\":26.92,\"quantity\":104,\"active\":false,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":48,\"sku\":\"SKU-765547\",\"name\":\"echo oscar\",\"status\":\"pending\",\"score\":72.91,\"quantity\":279,\"active\":true,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":49,\"sku\":\"SKU-440455\",\"name\":\"echo uniform\",\"status\":\"archived\",\"score\":74.63,\"quantity\":272,\"active\":false,\"updatedAt\":\"2026-06-22T12:00:00Z\"},{\"id\":50,\"sku\":\"SKU-303645\",\"name\":\"papa sierra\",\"status\":\"pending\",\"score\":28.63,\"quantity\":90,\"active\":false,\"updatedAt\":\"2026-06-25T12:00:00Z\"},{\"id\":51,\"sku\":\"SKU-213454\",\"name\":\"november oscar\",\"status\":\"pending\",\"score\":35.5,\"quantity\":159,\"active\":true,\"updatedAt\":\"2026-04-26T12:00:00Z\"},{\"id\":52,\"sku\":\"SKU-715886\",\"name\":\"mike oscar\",\"status\":\"active\",\"score\":73.01,\"quantity\":399,\"active\":true,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":53,\"sku\":\"SKU-586691\",\"name\":\"delta zulu\",\"status\":\"archived\",\"score\":48.69,\"quantity\":373,\"active\":false,\"updatedAt\":\"2026-02-10T12:00:00Z\"},{\"id\":54,\"sku\":\"SKU-236865\",\"name\":\"oscar romeo\",\"status\":\"archived\",\"score\":66.72,\"quantity\":241,\"active\":false,\"updatedAt\":\"2026-01-07T12:00:00Z\"},{\"id\":55,\"sku\":\"SKU-157251\",\"name\":\"alpha delta\",\"status\":\"archived\",\"score\":20.83,\"quantity\":58,\"active\":true,\"updatedAt\":\"2026-04-16T12:00:00Z\"},{\"id\":56,\"sku\":\"SKU-262253\",\"name\":\"golf echo\",\"status\":\"failed\",\"score\":33.33,\"quantity\":168,\"active\":false,\"updatedAt\":\"2026-03-12T12:00:00Z\"},{\"id\":57,\"sku\":\"SKU-863890\",\"name\":\"victor whiskey\",\"status\":\"archived\",\"score\":59.55,\"quantity\":199,\"active\":true,\"updatedAt\":\"2026-02-12T12:00:00Z\"},{\"id\":58,\"sku\":\"SKU-602861\",\"name\":\"bravo romeo\",\"status\":\"archived\",\"score\":62.56,\"quantity\":107,\"active\":false,\"updatedAt\":\"2026-02-14T12:00:00Z\"},{\"id\":59,\"sku\":\"SKU-140459\",\"name\":\"tango oscar\",\"status\":\"pending\",\"score\":6.22,\"quantity\":52,\"active\":false,\"updatedAt\":\"2026-01-21T12:00:00Z\"},{\"id\":60,\"sku\":\"SKU-736207\",\"name\":\"victor delta\",\"status\":\"active\",\"score\":44.62,\"quantity\":24,\"active\":true,\"updatedAt\":\"2026-03-13T12:00:00Z\"},{\"id\":61,\"sku\":\"SKU-759724\",\"name\":\"delta juliet\",\"status\":\"pending\",\"score\":26.46,\"quantity\":384,\"active\":false,\"updatedAt\":\"2026-05-19T12:00:00Z\"},{\"id\":62,\"sku\":\"SKU-442602\",\"name\":\"alpha golf\",\"status\":\"archived\",\"score\":49.58,\"quantity\":228,\"active\":true,\"updatedAt\":\"2026-05-05T12:00:00Z\"},{\"id\":63,\"sku\":\"SKU-575268\",\"name\":\"romeo foxtrot\",\"status\":\"archived\",\"score\":14.2,\"quantity\":267,\"active\":true,\"updatedAt\":\"2026-05-05T12:00:00Z\"},{\"id\":64,\"sku\":\"SKU-648994\",\"name\":\"hotel romeo\",\"status\":\"archived\",\"score\":44.89,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-03-11T12:00:00Z\"},{\"id\":65,\"sku\":\"SKU-188457\",\"name\":\"mike echo\",\"status\":\"active\",\"score\":94.79,\"quantity\":432,\"active\":false,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":66,\"sku\":\"SKU-149621\",\"name\":\"golf november\",\"status\":\"pending\",\"score\":90.25,\"quantity\":465,\"active\":false,\"updatedAt\":\"2026-03-11T12:00:00Z\"},{\"id\":67,\"sku\":\"SKU-285740\",\"name\":\"romeo charlie\",\"status\":\"pending\",\"score\":93.89,\"quantity\":5,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":68,\"sku\":\"SKU-291992\",\"name\":\"sierra lima\",\"status\":\"active\",\"score\":86.27,\"quantity\":85,\"active\":false,\"updatedAt\":\"2026-02-07T12:00:00Z\"},{\"id\":69,\"sku\":\"SKU-747919\",\"name\":\"golf whiskey\",\"status\":\"failed\",\"score\":90.75,\"quantity\":226,\"active\":false,\"updatedAt\":\"2026-04-09T12:00:00Z\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"id\":0,\"sku\":\"SKU-369981\",\"name\":\"lima oscar\",\"status\":\"failed\",\"score\":9.74,\"quantity\":27,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":1,\"sku\":\"SKU-967340\",\"name\":\"sierra kilo\",\"status\":\"pending\",\"score\":37.31,\"quantity\":46,\"active\":false,\"updatedAt\":\"2026-02-04T12:00:00Z\"},{\"id\":2,\"sku\":\"SKU-334172\",\"name\":\"mike golf\",\"status\":\"archived\",\"score\":94.56,\"quantity\":499,\"active\":false,\"updatedAt\":\"2026-06-24T12:00:00Z\"},{\"id\":3,\"sku\":\"SKU-708572\",\"name\":\"uniform kilo\",\"status\":\"failed\",\"score\":91.72,\"quantity\":414,\"active\":false,\"updatedAt\":\"2026-01-12T12:00:00Z\"},{\"id\":4,\"sku\":\"SKU-59452\",\"name\":\"alpha sierra\",\"status\":\"active\",\"score\":21.56,\"quantity\":215,\"active\":false,\"updatedAt\":\"2026-06-20T12:00:00Z\"},{\"id\":5,\"sku\":\"SKU-354103\",\"name\":\"zulu hotel\",\"status\":\"pending\",\"score\":59.22,\"quantity\":250,\"active\":true,\"updatedAt\":\"2026-05-02T12:00:00Z\"},{\"id\":6,\"sku\":\"SKU-868744\",\"name\":\"whiskey tango\",\"status\":\"active\",\"score\":46.31,\"quantity\":111,\"active\":true,\"updatedAt\":\"2026-01-05T12:00:00Z\"},{\"id\":7,\"sku\":\"SKU-834024\",\"name\":\"delta delta\",\"status\":\"archived\",\"score\":88.68,\"quantity\":221,\"active\":true,\"updatedAt\":\"2026-06-10T12:00:00Z\"},{\"id\":8,\"sku\":\"SKU-4318\",\"name\":\"alpha lima\",\"status\":\"failed\",\"score\":86.59,\"quantity\":237,\"active\":true,\"updatedAt\":\"2026-05-22T12:00:00Z\"},{\"id\":9,\"sku\":\"SKU-397052\",\"name\":\"bravo quebec\",\"status\":\"active\",\"score\":14.17,\"quantity\":244,\"active\":true,\"updatedAt\":\"2026-01-08T12:00:00Z\"},{\"id\":10,\"sku\":\"SKU-987401\",\"name\":\"november sierra\",\"status\":\"failed\",\"score\":67.5,\"quantity\":212,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":11,\"sku\":\"SKU-399261\",\"name\":\"charlie tango\",\"status\":\"active\",\"score\":12.8,\"quantity\":104,\"active\":false,\"updatedAt\":\"2026-05-01T12:00:00Z\"},{\"id\":12,\"sku\":\"SKU-541132\",\"name\":\"india november\",\"status\":\"active\",\"score\":8.18,\"quantity\":21,\"active\":true,\"updatedAt\":\"2026-02-08T12:00:00Z\"},{\"id\":13,\"sku\":\"SKU-664917\",\"name\":\"november golf\",\"status\":\"pending\",\"score\":39.06,\"quantity\":87,\"active\":true,\"updatedAt\":\"2026-02-22T12:00:00Z\"},{\"id\":14,\"sku\":\"SKU-470178\",\"name\":\"papa uniform\",\"status\":\"failed\",\"score\":31.51,\"quantity\":379,\"active\":true,\"updatedAt\":\"2026-04-28T12:00:00Z\"},{\"id\":15,\"sku\":\"SKU-795808\",\"name\":\"papa india\",\"status\":\"active\",\"score\":81.8,\"quantity\":443,\"active\":true,\"updatedAt\":\"2026-02-17T12:00:00Z\"},{\"id\":16,\"sku\":\"SKU-501650\",\"name\":\"golf india\",\"status\":\"pending\",\"score\":94.3,\"quantity\":462,\"active\":true,\"updatedAt\":\"2026-05-11T12:00:00Z\"},{\"id\":17,\"sku\":\"SKU-562629\",\"name\":\"charlie echo\",\"status\":\"archived\",\"score\":34.87,\"quantity\":280,\"active\":true,\"updatedAt\":\"2026-02-21T12:00:00Z\"},{\"id\":18,\"sku\":\"SKU-32189\",\"name\":\"zulu mike\",\"status\":\"pending\",\"score\":58.38,\"quantity\":372,\"active\":false,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":19,\"sku\":\"SKU-197718\",\"name\":\"india kilo\",\"status\":\"active\",\"score\":19.81,\"quantity\":296,\"active\":true,\"updatedAt\":\"2026-03-03T12:00:00Z\"},{\"id\":20,\"sku\":\"SKU-770813\",\"name\":\"whiskey delta\",\"status\":\"archived\",\"score\":14.07,\"quantity\":375,\"active\":false,\"updatedAt\":\"2026-06-11T12:00:00Z\"},{\"id\":21,\"sku\":\"SKU-993297\",\"name\":\"lima oscar\",\"status\":\"pending\",\"score\":49.46,\"quantity\":320,\"active\":true,\"updatedAt\":\"2026-06-04T12:00:00Z\"},{\"id\":22,\"sku\":\"SKU-129005\",\"name\":\"india delta\",\"status\":\"active\",\"score\":38.89,\"quantity\":1,\"active\":true,\"updatedAt\":\"2026-06-18T12:00:00Z\"},{\"id\":23,\"sku\":\"SKU-983730\",\"name\":\"bravo papa\",\"status\":\"pending\",\"score\":88.65,\"quantity\":367,\"active\":false,\"updatedAt\":\"2026-01-28T12:00:00Z\"},{\"id\":24,\"sku\":\"SKU-758767\",\"name\":\"romeo tango\",\"status\":\"pending\",\"score\":92.43,\"quantity\":327,\"active\":false,\"updatedAt\":\"2026-04-21T12:00:00Z\"},{\"id\":25,\"sku\":\"SKU-642530\",\"name\":\"charlie whiskey\",\"status\":\"failed\",\"score\":91.16,\"quantity\":139,\"active\":true,\"updatedAt\":\"2026-06-13T12:00:00Z\"},{\"id\":26,\"sku\":\"SKU-465260\",\"name\":\"kilo lima\",\"status\":\"archived\",\"score\":35.1,\"quantity\":257,\"active\":true,\"updatedAt\":\"2026-01-16T12:00:00Z\"},{\"id\":27,\"sku\":\"SKU-73648\",\"name\":\"india mike\",\"status\":\"archived\",\"score\":97.62,\"quantity\":386,\"active\":true,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":28,\"sku\":\"SKU-776786\",\"name\":\"victor lima\",\"status\":\"active\",\"score\":27.69,\"quantity\":438,\"active\":true,\"updatedAt\":\"2026-05-27T12:00:00Z\"},{\"id\":29,\"sku\":\"SKU-468983\",\"name\":\"hotel kilo\",\"status\":\"archived\",\"score\":0.48,\"quantity\":239,\"active\":true,\"updatedAt\":\"2026-02-09T12:00:00Z\"},{\"id\":30,\"sku\":\"SKU-563299\",\"name\":\"charlie november\",\"status\":\"archived\",\"score\":13.56,\"quantity\":88,\"active\":true,\"updatedAt\":\"2026-05-08T12:00:00Z\"},{\"id\":31,\"sku\":\"SKU-419442\",\"name\":\"romeo charlie\",\"status\":\"pending\",\"score\":26.86,\"quantity\":346,\"active\":true,\"updatedAt\":\"2026-04-15T12:00:00Z\"},{\"id\":32,\"sku\":\"SKU-182808\",\"name\":\"november tango\",\"status\":\"pending\",\"score\":95.74,\"quantity\":288,\"active\":false,\"updatedAt\":\"2026-04-25T12:00:00Z\"},{\"id\":33,\"sku\":\"SKU-921862\",\"name\":\"zulu sierra\",\"status\":\"active\",\"score\":38.59,\"quantity\":370,\"active\":true,\"updatedAt\":\"2026-02-02T12:00:00Z\"},{\"id\":34,\"sku\":\"SKU-809989\",\"name\":\"golf echo\",\"status\":\"archived\",\"score\":13.09,\"quantity\":427,\"active\":false,\"updatedAt\":\"2026-04-23T12:00:00Z\"},{\"id\":35,\"sku\":\"SKU-684809\",\"name\":\"romeo foxtrot\",\"status\":\"pending\",\"score\":82.4,\"quantity\":444,\"active\":false,\"updatedAt\":\"2026-03-19T12:00:00Z\"},{\"id\":36,\"sku\":\"SKU-56758\",\"name\":\"november victor\",\"status\":\"pending\",\"score\":57.61,\"quantity\":122,\"active\":true,\"updatedAt\":\"2026-02-06T12:00:00Z\"},{\"id\":37,\"sku\":\"SKU-158771\",\"name\":\"hotel quebec\",\"status\":\"archived\",\"score\":52.21,\"quantity\":108,\"active\":true,\"updatedAt\":\"2026-05-13T12:00:00Z\"},{\"id\":38,\"sku\":\"SKU-201657\",\"name\":\"india mike\",\"status\":\"archived\",\"score\":11.98,\"quantity\":279,\"active\":true,\"updatedAt\":\"2026-04-17T12:00:00Z\"},{\"id\":39,\"sku\":\"SKU-934265\",\"name\":\"oscar victor\",\"status\":\"failed\",\"score\":10.39,\"quantity\":10,\"active\":true,\"updatedAt\":\"2026-04-09T12:00:00Z\"},{\"id\":40,\"sku\":\"SKU-980470\",\"name\":\"delta zulu\",\"status\":\"failed\",\"score\":95.34,\"quantity\":73,\"active\":false,\"updatedAt\":\"2026-03-22T12:00:00Z\"},{\"id\":41,\"sku\":\"SKU-522215\",\"name\":\"november lima\",\"status\":\"archived\",\"score\":64.84,\"quantity\":369,\"active\":true,\"updatedAt\":\"2026-05-16T12:00:00Z\"},{\"id\":42,\"sku\":\"SKU-409943\",\"name\":\"hotel zulu\",\"status\":\"archived\",\"score\":11.64,\"quantity\":426,\"active\":true,\"updatedAt\":\"2026-02-11T12:00:00Z\"},{\"id\":43,\"sku\":\"SKU-77806\",\"name\":\"mike lima\",\"status\":\"active\",\"score\":63.83,\"quantity\":203,\"active\":true,\"updatedAt\":\"2026-03-19T12:00:00Z\"},{\"id\":44,\"sku\":\"SKU-928201\",\"name\":\"charlie mike\",\"status\":\"pending\",\"score\":5.06,\"quantity\":450,\"active\":false,\"updatedAt\":\"2026-05-06T12:00:00Z\"},{\"id\":45,\"sku\":\"SKU-619926\",\"name\":\"bravo whiskey\",\"status\":\"pending\",\"score\":20.01,\"quantity\":155,\"active\":true,\"updatedAt\":\"2026-02-06T12:00:00Z\"},{\"id\":46,\"sku\":\"SKU-22057\",\"name\":\"charlie sierra\",\"status\":\"failed\",\"score\":33.47,\"quantity\":435,\"active\":true,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":47,\"sku\":\"SKU-638993\",\"name\":\"alpha charlie\",\"status\":\"archived\",\"score\":26.92,\"quantity\":104,\"active\":false,\"updatedAt\":\"2026-05-07T12:00:00Z\"},{\"id\":48,\"sku\":\"SKU-765547\",\"name\":\"echo oscar\",\"status\":\"pending\",\"score\":72.91,\"quantity\":279,\"active\":true,\"updatedAt\":\"2026-02-13T12:00:00Z\"},{\"id\":49,\"sku\":\"SKU-440455\",\"name\":\"echo uniform\",\"status\":\"archived\",\"score\":74.63,\"quantity\":272,\"active\":false,\"updatedAt\":\"2026-06-22T12:00:00Z\"},{\"id\":50,\"sku\":\"SKU-303645\",\"name\":\"papa sierra\",\"status\":\"pending\",\"score\":28.63,\"quantity\":90,\"active\":false,\"updatedAt\":\"2026-06-25T12:00:00Z\"},{\"id\":51,\"sku\":\"SKU-213454\",\"name\":\"november oscar\",\"status\":\"pending\",\"score\":35.5,\"quantity\":159,\"active\":true,\"updatedAt\":\"2026-04-26T12:00:00Z\"},{\"id\":52,\"sku\":\"SKU-715886\",\"name\":\"mike oscar\",\"status\":\"active\",\"score\":73.01,\"quantity\":399,\"active\":true,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":53,\"sku\":\"SKU-586691\",\"name\":\"delta zulu\",\"status\":\"archived\",\"score\":48.69,\"quantity\":373,\"active\":false,\"updatedAt\":\"2026-02-10T12:00:00Z\"},{\"id\":54,\"sku\":\"SKU-236865\",\"name\":\"oscar romeo\",\"status\":\"archived\",\"score\":66.72,\"quantity\":241,\"active\":false,\"updatedAt\":\"2026-01-07T12:00:00Z\"},{\"id\":55,\"sku\":\"SKU-157251\",\"name\":\"alpha delta\",\"status\":\"archived\",\"score\":20.83,\"quantity\":58,\"active\":true,\"updatedAt\":\"2026-04-16T12:00:00Z\"},{\"id\":56,\"sku\":\"SKU-262253\",\"name\":\"golf echo\",\"status\":\"failed\",\"score\":33.33,\"quantity\":168,\"active\":false,\"updatedAt\":\"2026-03-12T12:00:00Z\"},{\"id\":57,\"sku\":\"SKU-863890\",\"name\":\"victor whiskey\",\"status\":\"archived\",\"score\":59.55,\"quantity\":199,\"active\":true,\"updatedAt\":\"2026-02-12T12:00:00Z\"},{\"id\":58,\"sku\":\"SKU-602861\",\"name\":\"bravo romeo\",\"status\":\"archived\",\"score\":62.56,\"quantity\":107,\"active\":false,\"updatedAt\":\"2026-02-14T12:00:00Z\"},{\"id\":59,\"sku\":\"SKU-140459\",\"name\":\"tango oscar\",\"status\":\"pending\",\"score\":6.22,\"quantity\":52,\"active\":false,\"updatedAt\":\"2026-01-21T12:00:00Z\"},{\"id\":60,\"sku\":\"SKU-736207\",\"name\":\"victor delta\",\"status\":\"active\",\"score\":44.62,\"quantity\":24,\"active\":true,\"updatedAt\":\"2026-03-13T12:00:00Z\"},{\"id\":61,\"sku\":\"SKU-759724\",\"name\":\"delta juliet\",\"status\":\"pending\",\"score\":26.46,\"quantity\":384,\"active\":false,\"updatedAt\":\"2026-05-19T12:00:00Z\"},{\"id\":62,\"sku\":\"SKU-442602\",\"name\":\"alpha golf\",\"status\":\"archived\",\"score\":49.58,\"quantity\":228,\"active\":true,\"updatedAt\":\"2026-05-05T12:00:00Z\"},{\"id\":63,\"sku\":\"SKU-575268\",\"name\":\"romeo foxtrot\",\"status\":\"archived\",\"score\":14.2,\"quantity\":267,\"active\":true,\"updatedAt\":\"2026-05-05T12:00:00Z\"},{\"id\":64,\"sku\":\"SKU-648994\",\"name\":\"hotel romeo\",\"status\":\"archived\",\"score\":44.89,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-03-11T12:00:00Z\"},{\"id\":65,\"sku\":\"SKU-188457\",\"name\":\"mike echo\",\"status\":\"active\",\"score\":94.79,\"quantity\":432,\"active\":false,\"updatedAt\":\"2026-05-21T12:00:00Z\"},{\"id\":66,\"sku\":\"SKU-149621\",\"name\":\"golf november\",\"status\":\"pending\",\"score\":90.25,\"quantity\":465,\"active\":false,\"updatedAt\":\"2026-03-11T12:00:00Z\"},{\"id\":67,\"sku\":\"SKU-285740\",\"name\":\"romeo charlie\",\"status\":\"pending\",\"score\":93.89,\"quantity\":5,\"active\":false,\"updatedAt\":\"2026-04-11T12:00:00Z\"},{\"id\":68,\"sku\":\"SKU-291992\",\"name\":\"sierra lima\",\"status\":\"active\",\"score\":86.27,\"quantity\":85,\"active\":false,\"updatedAt\":\"2026-02-07T12:00:00Z\"},{\"id\":69,\"sku\":\"SKU-747919\",\"name\":\"golf whiskey\",\"status\":\"failed\",\"score\":90.75,\"quantity\":226,\"active\":false,\"updatedAt\":\"2026-04-09T12:00:00Z\"}]", + "encoded": "[70]{id,sku,name,status,score,quantity,active,updatedAt}:\n 0,\"SKU-369981\",lima oscar,failed,9.74,27,false,\"2026-06-24T12:00:00Z\"\n 1,\"SKU-967340\",sierra kilo,pending,37.31,46,false,\"2026-02-04T12:00:00Z\"\n 2,\"SKU-334172\",mike golf,archived,94.56,499,false,\"2026-06-24T12:00:00Z\"\n 3,\"SKU-708572\",uniform kilo,failed,91.72,414,false,\"2026-01-12T12:00:00Z\"\n 4,\"SKU-59452\",alpha sierra,active,21.56,215,false,\"2026-06-20T12:00:00Z\"\n 5,\"SKU-354103\",zulu hotel,pending,59.22,250,true,\"2026-05-02T12:00:00Z\"\n 6,\"SKU-868744\",whiskey tango,active,46.31,111,true,\"2026-01-05T12:00:00Z\"\n 7,\"SKU-834024\",delta delta,archived,88.68,221,true,\"2026-06-10T12:00:00Z\"\n 8,\"SKU-4318\",alpha lima,failed,86.59,237,true,\"2026-05-22T12:00:00Z\"\n 9,\"SKU-397052\",bravo quebec,active,14.17,244,true,\"2026-01-08T12:00:00Z\"\n 10,\"SKU-987401\",november sierra,failed,67.5,212,true,\"2026-03-22T12:00:00Z\"\n 11,\"SKU-399261\",charlie tango,active,12.8,104,false,\"2026-05-01T12:00:00Z\"\n 12,\"SKU-541132\",india november,active,8.18,21,true,\"2026-02-08T12:00:00Z\"\n 13,\"SKU-664917\",november golf,pending,39.06,87,true,\"2026-02-22T12:00:00Z\"\n 14,\"SKU-470178\",papa uniform,failed,31.51,379,true,\"2026-04-28T12:00:00Z\"\n 15,\"SKU-795808\",papa india,active,81.8,443,true,\"2026-02-17T12:00:00Z\"\n 16,\"SKU-501650\",golf india,pending,94.3,462,true,\"2026-05-11T12:00:00Z\"\n 17,\"SKU-562629\",charlie echo,archived,34.87,280,true,\"2026-02-21T12:00:00Z\"\n 18,\"SKU-32189\",zulu mike,pending,58.38,372,false,\"2026-05-13T12:00:00Z\"\n 19,\"SKU-197718\",india kilo,active,19.81,296,true,\"2026-03-03T12:00:00Z\"\n 20,\"SKU-770813\",whiskey delta,archived,14.07,375,false,\"2026-06-11T12:00:00Z\"\n 21,\"SKU-993297\",lima oscar,pending,49.46,320,true,\"2026-06-04T12:00:00Z\"\n 22,\"SKU-129005\",india delta,active,38.89,1,true,\"2026-06-18T12:00:00Z\"\n 23,\"SKU-983730\",bravo papa,pending,88.65,367,false,\"2026-01-28T12:00:00Z\"\n 24,\"SKU-758767\",romeo tango,pending,92.43,327,false,\"2026-04-21T12:00:00Z\"\n 25,\"SKU-642530\",charlie whiskey,failed,91.16,139,true,\"2026-06-13T12:00:00Z\"\n 26,\"SKU-465260\",kilo lima,archived,35.1,257,true,\"2026-01-16T12:00:00Z\"\n 27,\"SKU-73648\",india mike,archived,97.62,386,true,\"2026-05-13T12:00:00Z\"\n 28,\"SKU-776786\",victor lima,active,27.69,438,true,\"2026-05-27T12:00:00Z\"\n 29,\"SKU-468983\",hotel kilo,archived,0.48,239,true,\"2026-02-09T12:00:00Z\"\n 30,\"SKU-563299\",charlie november,archived,13.56,88,true,\"2026-05-08T12:00:00Z\"\n 31,\"SKU-419442\",romeo charlie,pending,26.86,346,true,\"2026-04-15T12:00:00Z\"\n 32,\"SKU-182808\",november tango,pending,95.74,288,false,\"2026-04-25T12:00:00Z\"\n 33,\"SKU-921862\",zulu sierra,active,38.59,370,true,\"2026-02-02T12:00:00Z\"\n 34,\"SKU-809989\",golf echo,archived,13.09,427,false,\"2026-04-23T12:00:00Z\"\n 35,\"SKU-684809\",romeo foxtrot,pending,82.4,444,false,\"2026-03-19T12:00:00Z\"\n 36,\"SKU-56758\",november victor,pending,57.61,122,true,\"2026-02-06T12:00:00Z\"\n 37,\"SKU-158771\",hotel quebec,archived,52.21,108,true,\"2026-05-13T12:00:00Z\"\n 38,\"SKU-201657\",india mike,archived,11.98,279,true,\"2026-04-17T12:00:00Z\"\n 39,\"SKU-934265\",oscar victor,failed,10.39,10,true,\"2026-04-09T12:00:00Z\"\n 40,\"SKU-980470\",delta zulu,failed,95.34,73,false,\"2026-03-22T12:00:00Z\"\n 41,\"SKU-522215\",november lima,archived,64.84,369,true,\"2026-05-16T12:00:00Z\"\n 42,\"SKU-409943\",hotel zulu,archived,11.64,426,true,\"2026-02-11T12:00:00Z\"\n 43,\"SKU-77806\",mike lima,active,63.83,203,true,\"2026-03-19T12:00:00Z\"\n 44,\"SKU-928201\",charlie mike,pending,5.06,450,false,\"2026-05-06T12:00:00Z\"\n 45,\"SKU-619926\",bravo whiskey,pending,20.01,155,true,\"2026-02-06T12:00:00Z\"\n 46,\"SKU-22057\",charlie sierra,failed,33.47,435,true,\"2026-05-07T12:00:00Z\"\n 47,\"SKU-638993\",alpha charlie,archived,26.92,104,false,\"2026-05-07T12:00:00Z\"\n 48,\"SKU-765547\",echo oscar,pending,72.91,279,true,\"2026-02-13T12:00:00Z\"\n 49,\"SKU-440455\",echo uniform,archived,74.63,272,false,\"2026-06-22T12:00:00Z\"\n 50,\"SKU-303645\",papa sierra,pending,28.63,90,false,\"2026-06-25T12:00:00Z\"\n 51,\"SKU-213454\",november oscar,pending,35.5,159,true,\"2026-04-26T12:00:00Z\"\n 52,\"SKU-715886\",mike oscar,active,73.01,399,true,\"2026-05-21T12:00:00Z\"\n 53,\"SKU-586691\",delta zulu,archived,48.69,373,false,\"2026-02-10T12:00:00Z\"\n 54,\"SKU-236865\",oscar romeo,archived,66.72,241,false,\"2026-01-07T12:00:00Z\"\n 55,\"SKU-157251\",alpha delta,archived,20.83,58,true,\"2026-04-16T12:00:00Z\"\n 56,\"SKU-262253\",golf echo,failed,33.33,168,false,\"2026-03-12T12:00:00Z\"\n 57,\"SKU-863890\",victor whiskey,archived,59.55,199,true,\"2026-02-12T12:00:00Z\"\n 58,\"SKU-602861\",bravo romeo,archived,62.56,107,false,\"2026-02-14T12:00:00Z\"\n 59,\"SKU-140459\",tango oscar,pending,6.22,52,false,\"2026-01-21T12:00:00Z\"\n 60,\"SKU-736207\",victor delta,active,44.62,24,true,\"2026-03-13T12:00:00Z\"\n 61,\"SKU-759724\",delta juliet,pending,26.46,384,false,\"2026-05-19T12:00:00Z\"\n 62,\"SKU-442602\",alpha golf,archived,49.58,228,true,\"2026-05-05T12:00:00Z\"\n 63,\"SKU-575268\",romeo foxtrot,archived,14.2,267,true,\"2026-05-05T12:00:00Z\"\n 64,\"SKU-648994\",hotel romeo,archived,44.89,321,false,\"2026-03-11T12:00:00Z\"\n 65,\"SKU-188457\",mike echo,active,94.79,432,false,\"2026-05-21T12:00:00Z\"\n 66,\"SKU-149621\",golf november,pending,90.25,465,false,\"2026-03-11T12:00:00Z\"\n 67,\"SKU-285740\",romeo charlie,pending,93.89,5,false,\"2026-04-11T12:00:00Z\"\n 68,\"SKU-291992\",sierra lima,active,86.27,85,false,\"2026-02-07T12:00:00Z\"\n 69,\"SKU-747919\",golf whiskey,failed,90.75,226,false,\"2026-04-09T12:00:00Z\"" + } + }, + { + "name": "bench-10kb-2", + "rawContent": "{\"meta\":{\"source\":\"bench\",\"version\":3,\"total\":50},\"entries\":{\"entry_0\":{\"id\":0,\"sku\":\"SKU-509743\",\"name\":\"zulu charlie\",\"status\":\"archived\",\"score\":87.86,\"quantity\":46,\"active\":true,\"updatedAt\":\"2026-02-04T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"tango\"],\"depth\":2}},\"entry_1\":{\"id\":1,\"sku\":\"SKU-409669\",\"name\":\"quebec zulu\",\"status\":\"pending\",\"score\":9.37,\"quantity\":361,\"active\":false,\"updatedAt\":\"2026-06-27T12:00:00Z\",\"nested\":{\"tags\":[\"golf\",\"victor\"],\"depth\":2}},\"entry_2\":{\"id\":2,\"sku\":\"SKU-564643\",\"name\":\"romeo zulu\",\"status\":\"pending\",\"score\":5.29,\"quantity\":342,\"active\":true,\"updatedAt\":\"2026-03-28T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"alpha\"],\"depth\":2}},\"entry_3\":{\"id\":3,\"sku\":\"SKU-49672\",\"name\":\"delta romeo\",\"status\":\"pending\",\"score\":58.97,\"quantity\":72,\"active\":true,\"updatedAt\":\"2026-05-03T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"romeo\"],\"depth\":2}},\"entry_4\":{\"id\":4,\"sku\":\"SKU-461045\",\"name\":\"oscar echo\",\"status\":\"failed\",\"score\":84.05,\"quantity\":44,\"active\":true,\"updatedAt\":\"2026-05-01T12:00:00Z\",\"nested\":{\"tags\":[\"whiskey\",\"romeo\"],\"depth\":2}},\"entry_5\":{\"id\":5,\"sku\":\"SKU-434641\",\"name\":\"alpha foxtrot\",\"status\":\"pending\",\"score\":21.77,\"quantity\":262,\"active\":false,\"updatedAt\":\"2026-04-17T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"mike\"],\"depth\":2}},\"entry_6\":{\"id\":6,\"sku\":\"SKU-575426\",\"name\":\"victor golf\",\"status\":\"archived\",\"score\":88.95,\"quantity\":81,\"active\":true,\"updatedAt\":\"2026-01-25T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"alpha\"],\"depth\":2}},\"entry_7\":{\"id\":7,\"sku\":\"SKU-334021\",\"name\":\"romeo oscar\",\"status\":\"failed\",\"score\":16.58,\"quantity\":67,\"active\":false,\"updatedAt\":\"2026-05-25T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"whiskey\"],\"depth\":2}},\"entry_8\":{\"id\":8,\"sku\":\"SKU-540445\",\"name\":\"delta hotel\",\"status\":\"pending\",\"score\":53.51,\"quantity\":266,\"active\":true,\"updatedAt\":\"2026-03-16T12:00:00Z\",\"nested\":{\"tags\":[\"victor\",\"sierra\"],\"depth\":2}},\"entry_9\":{\"id\":9,\"sku\":\"SKU-256829\",\"name\":\"alpha zulu\",\"status\":\"pending\",\"score\":9.35,\"quantity\":292,\"active\":true,\"updatedAt\":\"2026-02-10T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"alpha\"],\"depth\":2}},\"entry_10\":{\"id\":10,\"sku\":\"SKU-950846\",\"name\":\"bravo foxtrot\",\"status\":\"active\",\"score\":49.13,\"quantity\":26,\"active\":true,\"updatedAt\":\"2026-01-04T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"india\"],\"depth\":2}},\"entry_11\":{\"id\":11,\"sku\":\"SKU-617755\",\"name\":\"mike zulu\",\"status\":\"pending\",\"score\":4.97,\"quantity\":415,\"active\":false,\"updatedAt\":\"2026-01-17T12:00:00Z\",\"nested\":{\"tags\":[\"mike\",\"echo\"],\"depth\":2}},\"entry_12\":{\"id\":12,\"sku\":\"SKU-581445\",\"name\":\"lima victor\",\"status\":\"archived\",\"score\":59.99,\"quantity\":485,\"active\":false,\"updatedAt\":\"2026-05-05T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"lima\"],\"depth\":2}},\"entry_13\":{\"id\":13,\"sku\":\"SKU-719044\",\"name\":\"oscar kilo\",\"status\":\"active\",\"score\":63.28,\"quantity\":24,\"active\":false,\"updatedAt\":\"2026-05-28T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"mike\"],\"depth\":2}},\"entry_14\":{\"id\":14,\"sku\":\"SKU-789713\",\"name\":\"papa oscar\",\"status\":\"archived\",\"score\":27.49,\"quantity\":198,\"active\":false,\"updatedAt\":\"2026-04-15T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"alpha\"],\"depth\":2}},\"entry_15\":{\"id\":15,\"sku\":\"SKU-534443\",\"name\":\"delta india\",\"status\":\"archived\",\"score\":62.51,\"quantity\":361,\"active\":false,\"updatedAt\":\"2026-05-12T12:00:00Z\",\"nested\":{\"tags\":[\"golf\",\"lima\"],\"depth\":2}},\"entry_16\":{\"id\":16,\"sku\":\"SKU-894299\",\"name\":\"delta golf\",\"status\":\"active\",\"score\":4.53,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-06-06T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"zulu\"],\"depth\":2}},\"entry_17\":{\"id\":17,\"sku\":\"SKU-178180\",\"name\":\"hotel oscar\",\"status\":\"active\",\"score\":94.67,\"quantity\":438,\"active\":false,\"updatedAt\":\"2026-04-12T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"quebec\"],\"depth\":2}},\"entry_18\":{\"id\":18,\"sku\":\"SKU-474295\",\"name\":\"november juliet\",\"status\":\"archived\",\"score\":52.97,\"quantity\":162,\"active\":true,\"updatedAt\":\"2026-06-12T12:00:00Z\",\"nested\":{\"tags\":[\"foxtrot\",\"romeo\"],\"depth\":2}},\"entry_19\":{\"id\":19,\"sku\":\"SKU-526106\",\"name\":\"golf romeo\",\"status\":\"archived\",\"score\":51.51,\"quantity\":176,\"active\":true,\"updatedAt\":\"2026-01-14T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"quebec\"],\"depth\":2}},\"entry_20\":{\"id\":20,\"sku\":\"SKU-643873\",\"name\":\"foxtrot tango\",\"status\":\"pending\",\"score\":84.13,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-05-05T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"uniform\"],\"depth\":2}},\"entry_21\":{\"id\":21,\"sku\":\"SKU-881747\",\"name\":\"india charlie\",\"status\":\"active\",\"score\":99.94,\"quantity\":215,\"active\":false,\"updatedAt\":\"2026-03-09T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"delta\"],\"depth\":2}},\"entry_22\":{\"id\":22,\"sku\":\"SKU-536249\",\"name\":\"oscar echo\",\"status\":\"pending\",\"score\":49.7,\"quantity\":198,\"active\":true,\"updatedAt\":\"2026-02-23T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"juliet\"],\"depth\":2}},\"entry_23\":{\"id\":23,\"sku\":\"SKU-242438\",\"name\":\"zulu alpha\",\"status\":\"failed\",\"score\":15.35,\"quantity\":282,\"active\":false,\"updatedAt\":\"2026-01-11T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"kilo\"],\"depth\":2}},\"entry_24\":{\"id\":24,\"sku\":\"SKU-787964\",\"name\":\"oscar hotel\",\"status\":\"archived\",\"score\":35.78,\"quantity\":383,\"active\":true,\"updatedAt\":\"2026-04-21T12:00:00Z\",\"nested\":{\"tags\":[\"zulu\",\"uniform\"],\"depth\":2}},\"entry_25\":{\"id\":25,\"sku\":\"SKU-270620\",\"name\":\"india india\",\"status\":\"pending\",\"score\":30.89,\"quantity\":367,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"echo\"],\"depth\":2}},\"entry_26\":{\"id\":26,\"sku\":\"SKU-479773\",\"name\":\"whiskey papa\",\"status\":\"archived\",\"score\":90.09,\"quantity\":10,\"active\":false,\"updatedAt\":\"2026-06-16T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"foxtrot\"],\"depth\":2}},\"entry_27\":{\"id\":27,\"sku\":\"SKU-85068\",\"name\":\"kilo zulu\",\"status\":\"archived\",\"score\":11.57,\"quantity\":100,\"active\":true,\"updatedAt\":\"2026-04-01T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"juliet\"],\"depth\":2}},\"entry_28\":{\"id\":28,\"sku\":\"SKU-88619\",\"name\":\"alpha echo\",\"status\":\"pending\",\"score\":86.22,\"quantity\":23,\"active\":true,\"updatedAt\":\"2026-03-02T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"victor\"],\"depth\":2}},\"entry_29\":{\"id\":29,\"sku\":\"SKU-342557\",\"name\":\"bravo papa\",\"status\":\"failed\",\"score\":57.84,\"quantity\":493,\"active\":false,\"updatedAt\":\"2026-05-15T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"foxtrot\"],\"depth\":2}},\"entry_30\":{\"id\":30,\"sku\":\"SKU-64537\",\"name\":\"quebec charlie\",\"status\":\"failed\",\"score\":65.9,\"quantity\":334,\"active\":false,\"updatedAt\":\"2026-04-15T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"juliet\"],\"depth\":2}},\"entry_31\":{\"id\":31,\"sku\":\"SKU-45365\",\"name\":\"papa golf\",\"status\":\"pending\",\"score\":56.24,\"quantity\":315,\"active\":false,\"updatedAt\":\"2026-05-07T12:00:00Z\",\"nested\":{\"tags\":[\"bravo\",\"charlie\"],\"depth\":2}},\"entry_32\":{\"id\":32,\"sku\":\"SKU-306983\",\"name\":\"quebec foxtrot\",\"status\":\"active\",\"score\":6.96,\"quantity\":276,\"active\":true,\"updatedAt\":\"2026-02-17T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"victor\"],\"depth\":2}},\"entry_33\":{\"id\":33,\"sku\":\"SKU-100839\",\"name\":\"alpha lima\",\"status\":\"active\",\"score\":7.02,\"quantity\":308,\"active\":true,\"updatedAt\":\"2026-03-01T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"victor\"],\"depth\":2}},\"entry_34\":{\"id\":34,\"sku\":\"SKU-285603\",\"name\":\"quebec echo\",\"status\":\"pending\",\"score\":54.61,\"quantity\":276,\"active\":false,\"updatedAt\":\"2026-04-12T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"kilo\"],\"depth\":2}},\"entry_35\":{\"id\":35,\"sku\":\"SKU-129333\",\"name\":\"foxtrot delta\",\"status\":\"pending\",\"score\":0.69,\"quantity\":36,\"active\":true,\"updatedAt\":\"2026-02-11T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"sierra\"],\"depth\":2}},\"entry_36\":{\"id\":36,\"sku\":\"SKU-704370\",\"name\":\"sierra oscar\",\"status\":\"active\",\"score\":45.65,\"quantity\":239,\"active\":false,\"updatedAt\":\"2026-01-22T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"oscar\"],\"depth\":2}},\"entry_37\":{\"id\":37,\"sku\":\"SKU-810963\",\"name\":\"foxtrot alpha\",\"status\":\"failed\",\"score\":59.48,\"quantity\":48,\"active\":true,\"updatedAt\":\"2026-06-24T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"zulu\"],\"depth\":2}},\"entry_38\":{\"id\":38,\"sku\":\"SKU-171741\",\"name\":\"zulu uniform\",\"status\":\"failed\",\"score\":42.96,\"quantity\":48,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"charlie\"],\"depth\":2}},\"entry_39\":{\"id\":39,\"sku\":\"SKU-893779\",\"name\":\"alpha tango\",\"status\":\"archived\",\"score\":0.81,\"quantity\":390,\"active\":false,\"updatedAt\":\"2026-06-25T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"victor\"],\"depth\":2}},\"entry_40\":{\"id\":40,\"sku\":\"SKU-455048\",\"name\":\"quebec uniform\",\"status\":\"archived\",\"score\":52.81,\"quantity\":296,\"active\":true,\"updatedAt\":\"2026-03-11T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"kilo\"],\"depth\":2}},\"entry_41\":{\"id\":41,\"sku\":\"SKU-579597\",\"name\":\"alpha bravo\",\"status\":\"active\",\"score\":74.63,\"quantity\":209,\"active\":false,\"updatedAt\":\"2026-02-05T12:00:00Z\",\"nested\":{\"tags\":[\"papa\",\"charlie\"],\"depth\":2}},\"entry_42\":{\"id\":42,\"sku\":\"SKU-927995\",\"name\":\"bravo oscar\",\"status\":\"failed\",\"score\":46.03,\"quantity\":13,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"uniform\"],\"depth\":2}},\"entry_43\":{\"id\":43,\"sku\":\"SKU-640313\",\"name\":\"oscar foxtrot\",\"status\":\"failed\",\"score\":75.06,\"quantity\":102,\"active\":true,\"updatedAt\":\"2026-06-10T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"quebec\"],\"depth\":2}},\"entry_44\":{\"id\":44,\"sku\":\"SKU-587339\",\"name\":\"delta lima\",\"status\":\"pending\",\"score\":1.02,\"quantity\":160,\"active\":false,\"updatedAt\":\"2026-03-12T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"november\"],\"depth\":2}},\"entry_45\":{\"id\":45,\"sku\":\"SKU-22977\",\"name\":\"alpha charlie\",\"status\":\"active\",\"score\":91.65,\"quantity\":457,\"active\":false,\"updatedAt\":\"2026-02-26T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"kilo\"],\"depth\":2}},\"entry_46\":{\"id\":46,\"sku\":\"SKU-274156\",\"name\":\"foxtrot oscar\",\"status\":\"archived\",\"score\":31.37,\"quantity\":424,\"active\":true,\"updatedAt\":\"2026-01-24T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"foxtrot\"],\"depth\":2}},\"entry_47\":{\"id\":47,\"sku\":\"SKU-533189\",\"name\":\"golf lima\",\"status\":\"active\",\"score\":9.71,\"quantity\":251,\"active\":false,\"updatedAt\":\"2026-04-01T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"kilo\"],\"depth\":2}},\"entry_48\":{\"id\":48,\"sku\":\"SKU-290356\",\"name\":\"zulu zulu\",\"status\":\"archived\",\"score\":25.89,\"quantity\":432,\"active\":false,\"updatedAt\":\"2026-04-14T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"kilo\"],\"depth\":2}},\"entry_49\":{\"id\":49,\"sku\":\"SKU-746759\",\"name\":\"juliet tango\",\"status\":\"active\",\"score\":40.83,\"quantity\":374,\"active\":false,\"updatedAt\":\"2026-06-22T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"golf\"],\"depth\":2}}}}", + "unwrap": true, + "expected": { + "normalized": "{\"meta\":{\"source\":\"bench\",\"version\":3,\"total\":50},\"entries\":{\"entry_0\":{\"id\":0,\"sku\":\"SKU-509743\",\"name\":\"zulu charlie\",\"status\":\"archived\",\"score\":87.86,\"quantity\":46,\"active\":true,\"updatedAt\":\"2026-02-04T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"tango\"],\"depth\":2}},\"entry_1\":{\"id\":1,\"sku\":\"SKU-409669\",\"name\":\"quebec zulu\",\"status\":\"pending\",\"score\":9.37,\"quantity\":361,\"active\":false,\"updatedAt\":\"2026-06-27T12:00:00Z\",\"nested\":{\"tags\":[\"golf\",\"victor\"],\"depth\":2}},\"entry_2\":{\"id\":2,\"sku\":\"SKU-564643\",\"name\":\"romeo zulu\",\"status\":\"pending\",\"score\":5.29,\"quantity\":342,\"active\":true,\"updatedAt\":\"2026-03-28T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"alpha\"],\"depth\":2}},\"entry_3\":{\"id\":3,\"sku\":\"SKU-49672\",\"name\":\"delta romeo\",\"status\":\"pending\",\"score\":58.97,\"quantity\":72,\"active\":true,\"updatedAt\":\"2026-05-03T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"romeo\"],\"depth\":2}},\"entry_4\":{\"id\":4,\"sku\":\"SKU-461045\",\"name\":\"oscar echo\",\"status\":\"failed\",\"score\":84.05,\"quantity\":44,\"active\":true,\"updatedAt\":\"2026-05-01T12:00:00Z\",\"nested\":{\"tags\":[\"whiskey\",\"romeo\"],\"depth\":2}},\"entry_5\":{\"id\":5,\"sku\":\"SKU-434641\",\"name\":\"alpha foxtrot\",\"status\":\"pending\",\"score\":21.77,\"quantity\":262,\"active\":false,\"updatedAt\":\"2026-04-17T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"mike\"],\"depth\":2}},\"entry_6\":{\"id\":6,\"sku\":\"SKU-575426\",\"name\":\"victor golf\",\"status\":\"archived\",\"score\":88.95,\"quantity\":81,\"active\":true,\"updatedAt\":\"2026-01-25T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"alpha\"],\"depth\":2}},\"entry_7\":{\"id\":7,\"sku\":\"SKU-334021\",\"name\":\"romeo oscar\",\"status\":\"failed\",\"score\":16.58,\"quantity\":67,\"active\":false,\"updatedAt\":\"2026-05-25T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"whiskey\"],\"depth\":2}},\"entry_8\":{\"id\":8,\"sku\":\"SKU-540445\",\"name\":\"delta hotel\",\"status\":\"pending\",\"score\":53.51,\"quantity\":266,\"active\":true,\"updatedAt\":\"2026-03-16T12:00:00Z\",\"nested\":{\"tags\":[\"victor\",\"sierra\"],\"depth\":2}},\"entry_9\":{\"id\":9,\"sku\":\"SKU-256829\",\"name\":\"alpha zulu\",\"status\":\"pending\",\"score\":9.35,\"quantity\":292,\"active\":true,\"updatedAt\":\"2026-02-10T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"alpha\"],\"depth\":2}},\"entry_10\":{\"id\":10,\"sku\":\"SKU-950846\",\"name\":\"bravo foxtrot\",\"status\":\"active\",\"score\":49.13,\"quantity\":26,\"active\":true,\"updatedAt\":\"2026-01-04T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"india\"],\"depth\":2}},\"entry_11\":{\"id\":11,\"sku\":\"SKU-617755\",\"name\":\"mike zulu\",\"status\":\"pending\",\"score\":4.97,\"quantity\":415,\"active\":false,\"updatedAt\":\"2026-01-17T12:00:00Z\",\"nested\":{\"tags\":[\"mike\",\"echo\"],\"depth\":2}},\"entry_12\":{\"id\":12,\"sku\":\"SKU-581445\",\"name\":\"lima victor\",\"status\":\"archived\",\"score\":59.99,\"quantity\":485,\"active\":false,\"updatedAt\":\"2026-05-05T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"lima\"],\"depth\":2}},\"entry_13\":{\"id\":13,\"sku\":\"SKU-719044\",\"name\":\"oscar kilo\",\"status\":\"active\",\"score\":63.28,\"quantity\":24,\"active\":false,\"updatedAt\":\"2026-05-28T12:00:00Z\",\"nested\":{\"tags\":[\"hotel\",\"mike\"],\"depth\":2}},\"entry_14\":{\"id\":14,\"sku\":\"SKU-789713\",\"name\":\"papa oscar\",\"status\":\"archived\",\"score\":27.49,\"quantity\":198,\"active\":false,\"updatedAt\":\"2026-04-15T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"alpha\"],\"depth\":2}},\"entry_15\":{\"id\":15,\"sku\":\"SKU-534443\",\"name\":\"delta india\",\"status\":\"archived\",\"score\":62.51,\"quantity\":361,\"active\":false,\"updatedAt\":\"2026-05-12T12:00:00Z\",\"nested\":{\"tags\":[\"golf\",\"lima\"],\"depth\":2}},\"entry_16\":{\"id\":16,\"sku\":\"SKU-894299\",\"name\":\"delta golf\",\"status\":\"active\",\"score\":4.53,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-06-06T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"zulu\"],\"depth\":2}},\"entry_17\":{\"id\":17,\"sku\":\"SKU-178180\",\"name\":\"hotel oscar\",\"status\":\"active\",\"score\":94.67,\"quantity\":438,\"active\":false,\"updatedAt\":\"2026-04-12T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"quebec\"],\"depth\":2}},\"entry_18\":{\"id\":18,\"sku\":\"SKU-474295\",\"name\":\"november juliet\",\"status\":\"archived\",\"score\":52.97,\"quantity\":162,\"active\":true,\"updatedAt\":\"2026-06-12T12:00:00Z\",\"nested\":{\"tags\":[\"foxtrot\",\"romeo\"],\"depth\":2}},\"entry_19\":{\"id\":19,\"sku\":\"SKU-526106\",\"name\":\"golf romeo\",\"status\":\"archived\",\"score\":51.51,\"quantity\":176,\"active\":true,\"updatedAt\":\"2026-01-14T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"quebec\"],\"depth\":2}},\"entry_20\":{\"id\":20,\"sku\":\"SKU-643873\",\"name\":\"foxtrot tango\",\"status\":\"pending\",\"score\":84.13,\"quantity\":321,\"active\":false,\"updatedAt\":\"2026-05-05T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"uniform\"],\"depth\":2}},\"entry_21\":{\"id\":21,\"sku\":\"SKU-881747\",\"name\":\"india charlie\",\"status\":\"active\",\"score\":99.94,\"quantity\":215,\"active\":false,\"updatedAt\":\"2026-03-09T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"delta\"],\"depth\":2}},\"entry_22\":{\"id\":22,\"sku\":\"SKU-536249\",\"name\":\"oscar echo\",\"status\":\"pending\",\"score\":49.7,\"quantity\":198,\"active\":true,\"updatedAt\":\"2026-02-23T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"juliet\"],\"depth\":2}},\"entry_23\":{\"id\":23,\"sku\":\"SKU-242438\",\"name\":\"zulu alpha\",\"status\":\"failed\",\"score\":15.35,\"quantity\":282,\"active\":false,\"updatedAt\":\"2026-01-11T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"kilo\"],\"depth\":2}},\"entry_24\":{\"id\":24,\"sku\":\"SKU-787964\",\"name\":\"oscar hotel\",\"status\":\"archived\",\"score\":35.78,\"quantity\":383,\"active\":true,\"updatedAt\":\"2026-04-21T12:00:00Z\",\"nested\":{\"tags\":[\"zulu\",\"uniform\"],\"depth\":2}},\"entry_25\":{\"id\":25,\"sku\":\"SKU-270620\",\"name\":\"india india\",\"status\":\"pending\",\"score\":30.89,\"quantity\":367,\"active\":false,\"updatedAt\":\"2026-04-10T12:00:00Z\",\"nested\":{\"tags\":[\"tango\",\"echo\"],\"depth\":2}},\"entry_26\":{\"id\":26,\"sku\":\"SKU-479773\",\"name\":\"whiskey papa\",\"status\":\"archived\",\"score\":90.09,\"quantity\":10,\"active\":false,\"updatedAt\":\"2026-06-16T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"foxtrot\"],\"depth\":2}},\"entry_27\":{\"id\":27,\"sku\":\"SKU-85068\",\"name\":\"kilo zulu\",\"status\":\"archived\",\"score\":11.57,\"quantity\":100,\"active\":true,\"updatedAt\":\"2026-04-01T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"juliet\"],\"depth\":2}},\"entry_28\":{\"id\":28,\"sku\":\"SKU-88619\",\"name\":\"alpha echo\",\"status\":\"pending\",\"score\":86.22,\"quantity\":23,\"active\":true,\"updatedAt\":\"2026-03-02T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"victor\"],\"depth\":2}},\"entry_29\":{\"id\":29,\"sku\":\"SKU-342557\",\"name\":\"bravo papa\",\"status\":\"failed\",\"score\":57.84,\"quantity\":493,\"active\":false,\"updatedAt\":\"2026-05-15T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"foxtrot\"],\"depth\":2}},\"entry_30\":{\"id\":30,\"sku\":\"SKU-64537\",\"name\":\"quebec charlie\",\"status\":\"failed\",\"score\":65.9,\"quantity\":334,\"active\":false,\"updatedAt\":\"2026-04-15T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"juliet\"],\"depth\":2}},\"entry_31\":{\"id\":31,\"sku\":\"SKU-45365\",\"name\":\"papa golf\",\"status\":\"pending\",\"score\":56.24,\"quantity\":315,\"active\":false,\"updatedAt\":\"2026-05-07T12:00:00Z\",\"nested\":{\"tags\":[\"bravo\",\"charlie\"],\"depth\":2}},\"entry_32\":{\"id\":32,\"sku\":\"SKU-306983\",\"name\":\"quebec foxtrot\",\"status\":\"active\",\"score\":6.96,\"quantity\":276,\"active\":true,\"updatedAt\":\"2026-02-17T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"victor\"],\"depth\":2}},\"entry_33\":{\"id\":33,\"sku\":\"SKU-100839\",\"name\":\"alpha lima\",\"status\":\"active\",\"score\":7.02,\"quantity\":308,\"active\":true,\"updatedAt\":\"2026-03-01T12:00:00Z\",\"nested\":{\"tags\":[\"uniform\",\"victor\"],\"depth\":2}},\"entry_34\":{\"id\":34,\"sku\":\"SKU-285603\",\"name\":\"quebec echo\",\"status\":\"pending\",\"score\":54.61,\"quantity\":276,\"active\":false,\"updatedAt\":\"2026-04-12T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"kilo\"],\"depth\":2}},\"entry_35\":{\"id\":35,\"sku\":\"SKU-129333\",\"name\":\"foxtrot delta\",\"status\":\"pending\",\"score\":0.69,\"quantity\":36,\"active\":true,\"updatedAt\":\"2026-02-11T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"sierra\"],\"depth\":2}},\"entry_36\":{\"id\":36,\"sku\":\"SKU-704370\",\"name\":\"sierra oscar\",\"status\":\"active\",\"score\":45.65,\"quantity\":239,\"active\":false,\"updatedAt\":\"2026-01-22T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"oscar\"],\"depth\":2}},\"entry_37\":{\"id\":37,\"sku\":\"SKU-810963\",\"name\":\"foxtrot alpha\",\"status\":\"failed\",\"score\":59.48,\"quantity\":48,\"active\":true,\"updatedAt\":\"2026-06-24T12:00:00Z\",\"nested\":{\"tags\":[\"juliet\",\"zulu\"],\"depth\":2}},\"entry_38\":{\"id\":38,\"sku\":\"SKU-171741\",\"name\":\"zulu uniform\",\"status\":\"failed\",\"score\":42.96,\"quantity\":48,\"active\":true,\"updatedAt\":\"2026-03-22T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"charlie\"],\"depth\":2}},\"entry_39\":{\"id\":39,\"sku\":\"SKU-893779\",\"name\":\"alpha tango\",\"status\":\"archived\",\"score\":0.81,\"quantity\":390,\"active\":false,\"updatedAt\":\"2026-06-25T12:00:00Z\",\"nested\":{\"tags\":[\"echo\",\"victor\"],\"depth\":2}},\"entry_40\":{\"id\":40,\"sku\":\"SKU-455048\",\"name\":\"quebec uniform\",\"status\":\"archived\",\"score\":52.81,\"quantity\":296,\"active\":true,\"updatedAt\":\"2026-03-11T12:00:00Z\",\"nested\":{\"tags\":[\"quebec\",\"kilo\"],\"depth\":2}},\"entry_41\":{\"id\":41,\"sku\":\"SKU-579597\",\"name\":\"alpha bravo\",\"status\":\"active\",\"score\":74.63,\"quantity\":209,\"active\":false,\"updatedAt\":\"2026-02-05T12:00:00Z\",\"nested\":{\"tags\":[\"papa\",\"charlie\"],\"depth\":2}},\"entry_42\":{\"id\":42,\"sku\":\"SKU-927995\",\"name\":\"bravo oscar\",\"status\":\"failed\",\"score\":46.03,\"quantity\":13,\"active\":true,\"updatedAt\":\"2026-01-18T12:00:00Z\",\"nested\":{\"tags\":[\"romeo\",\"uniform\"],\"depth\":2}},\"entry_43\":{\"id\":43,\"sku\":\"SKU-640313\",\"name\":\"oscar foxtrot\",\"status\":\"failed\",\"score\":75.06,\"quantity\":102,\"active\":true,\"updatedAt\":\"2026-06-10T12:00:00Z\",\"nested\":{\"tags\":[\"november\",\"quebec\"],\"depth\":2}},\"entry_44\":{\"id\":44,\"sku\":\"SKU-587339\",\"name\":\"delta lima\",\"status\":\"pending\",\"score\":1.02,\"quantity\":160,\"active\":false,\"updatedAt\":\"2026-03-12T12:00:00Z\",\"nested\":{\"tags\":[\"kilo\",\"november\"],\"depth\":2}},\"entry_45\":{\"id\":45,\"sku\":\"SKU-22977\",\"name\":\"alpha charlie\",\"status\":\"active\",\"score\":91.65,\"quantity\":457,\"active\":false,\"updatedAt\":\"2026-02-26T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"kilo\"],\"depth\":2}},\"entry_46\":{\"id\":46,\"sku\":\"SKU-274156\",\"name\":\"foxtrot oscar\",\"status\":\"archived\",\"score\":31.37,\"quantity\":424,\"active\":true,\"updatedAt\":\"2026-01-24T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"foxtrot\"],\"depth\":2}},\"entry_47\":{\"id\":47,\"sku\":\"SKU-533189\",\"name\":\"golf lima\",\"status\":\"active\",\"score\":9.71,\"quantity\":251,\"active\":false,\"updatedAt\":\"2026-04-01T12:00:00Z\",\"nested\":{\"tags\":[\"india\",\"kilo\"],\"depth\":2}},\"entry_48\":{\"id\":48,\"sku\":\"SKU-290356\",\"name\":\"zulu zulu\",\"status\":\"archived\",\"score\":25.89,\"quantity\":432,\"active\":false,\"updatedAt\":\"2026-04-14T12:00:00Z\",\"nested\":{\"tags\":[\"alpha\",\"kilo\"],\"depth\":2}},\"entry_49\":{\"id\":49,\"sku\":\"SKU-746759\",\"name\":\"juliet tango\",\"status\":\"active\",\"score\":40.83,\"quantity\":374,\"active\":false,\"updatedAt\":\"2026-06-22T12:00:00Z\",\"nested\":{\"tags\":[\"delta\",\"golf\"],\"depth\":2}}}}", + "encoded": "meta:\n source: bench\n version: 3\n total: 50\nentries:\n entry_0:\n id: 0\n sku: \"SKU-509743\"\n name: zulu charlie\n status: archived\n score: 87.86\n quantity: 46\n active: true\n updatedAt: \"2026-02-04T12:00:00Z\"\n nested:\n tags[2]: november,tango\n depth: 2\n entry_1:\n id: 1\n sku: \"SKU-409669\"\n name: quebec zulu\n status: pending\n score: 9.37\n quantity: 361\n active: false\n updatedAt: \"2026-06-27T12:00:00Z\"\n nested:\n tags[2]: golf,victor\n depth: 2\n entry_2:\n id: 2\n sku: \"SKU-564643\"\n name: romeo zulu\n status: pending\n score: 5.29\n quantity: 342\n active: true\n updatedAt: \"2026-03-28T12:00:00Z\"\n nested:\n tags[2]: echo,alpha\n depth: 2\n entry_3:\n id: 3\n sku: \"SKU-49672\"\n name: delta romeo\n status: pending\n score: 58.97\n quantity: 72\n active: true\n updatedAt: \"2026-05-03T12:00:00Z\"\n nested:\n tags[2]: delta,romeo\n depth: 2\n entry_4:\n id: 4\n sku: \"SKU-461045\"\n name: oscar echo\n status: failed\n score: 84.05\n quantity: 44\n active: true\n updatedAt: \"2026-05-01T12:00:00Z\"\n nested:\n tags[2]: whiskey,romeo\n depth: 2\n entry_5:\n id: 5\n sku: \"SKU-434641\"\n name: alpha foxtrot\n status: pending\n score: 21.77\n quantity: 262\n active: false\n updatedAt: \"2026-04-17T12:00:00Z\"\n nested:\n tags[2]: hotel,mike\n depth: 2\n entry_6:\n id: 6\n sku: \"SKU-575426\"\n name: victor golf\n status: archived\n score: 88.95\n quantity: 81\n active: true\n updatedAt: \"2026-01-25T12:00:00Z\"\n nested:\n tags[2]: juliet,alpha\n depth: 2\n entry_7:\n id: 7\n sku: \"SKU-334021\"\n name: romeo oscar\n status: failed\n score: 16.58\n quantity: 67\n active: false\n updatedAt: \"2026-05-25T12:00:00Z\"\n nested:\n tags[2]: juliet,whiskey\n depth: 2\n entry_8:\n id: 8\n sku: \"SKU-540445\"\n name: delta hotel\n status: pending\n score: 53.51\n quantity: 266\n active: true\n updatedAt: \"2026-03-16T12:00:00Z\"\n nested:\n tags[2]: victor,sierra\n depth: 2\n entry_9:\n id: 9\n sku: \"SKU-256829\"\n name: alpha zulu\n status: pending\n score: 9.35\n quantity: 292\n active: true\n updatedAt: \"2026-02-10T12:00:00Z\"\n nested:\n tags[2]: romeo,alpha\n depth: 2\n entry_10:\n id: 10\n sku: \"SKU-950846\"\n name: bravo foxtrot\n status: active\n score: 49.13\n quantity: 26\n active: true\n updatedAt: \"2026-01-04T12:00:00Z\"\n nested:\n tags[2]: delta,india\n depth: 2\n entry_11:\n id: 11\n sku: \"SKU-617755\"\n name: mike zulu\n status: pending\n score: 4.97\n quantity: 415\n active: false\n updatedAt: \"2026-01-17T12:00:00Z\"\n nested:\n tags[2]: mike,echo\n depth: 2\n entry_12:\n id: 12\n sku: \"SKU-581445\"\n name: lima victor\n status: archived\n score: 59.99\n quantity: 485\n active: false\n updatedAt: \"2026-05-05T12:00:00Z\"\n nested:\n tags[2]: alpha,lima\n depth: 2\n entry_13:\n id: 13\n sku: \"SKU-719044\"\n name: oscar kilo\n status: active\n score: 63.28\n quantity: 24\n active: false\n updatedAt: \"2026-05-28T12:00:00Z\"\n nested:\n tags[2]: hotel,mike\n depth: 2\n entry_14:\n id: 14\n sku: \"SKU-789713\"\n name: papa oscar\n status: archived\n score: 27.49\n quantity: 198\n active: false\n updatedAt: \"2026-04-15T12:00:00Z\"\n nested:\n tags[2]: juliet,alpha\n depth: 2\n entry_15:\n id: 15\n sku: \"SKU-534443\"\n name: delta india\n status: archived\n score: 62.51\n quantity: 361\n active: false\n updatedAt: \"2026-05-12T12:00:00Z\"\n nested:\n tags[2]: golf,lima\n depth: 2\n entry_16:\n id: 16\n sku: \"SKU-894299\"\n name: delta golf\n status: active\n score: 4.53\n quantity: 321\n active: false\n updatedAt: \"2026-06-06T12:00:00Z\"\n nested:\n tags[2]: uniform,zulu\n depth: 2\n entry_17:\n id: 17\n sku: \"SKU-178180\"\n name: hotel oscar\n status: active\n score: 94.67\n quantity: 438\n active: false\n updatedAt: \"2026-04-12T12:00:00Z\"\n nested:\n tags[2]: romeo,quebec\n depth: 2\n entry_18:\n id: 18\n sku: \"SKU-474295\"\n name: november juliet\n status: archived\n score: 52.97\n quantity: 162\n active: true\n updatedAt: \"2026-06-12T12:00:00Z\"\n nested:\n tags[2]: foxtrot,romeo\n depth: 2\n entry_19:\n id: 19\n sku: \"SKU-526106\"\n name: golf romeo\n status: archived\n score: 51.51\n quantity: 176\n active: true\n updatedAt: \"2026-01-14T12:00:00Z\"\n nested:\n tags[2]: tango,quebec\n depth: 2\n entry_20:\n id: 20\n sku: \"SKU-643873\"\n name: foxtrot tango\n status: pending\n score: 84.13\n quantity: 321\n active: false\n updatedAt: \"2026-05-05T12:00:00Z\"\n nested:\n tags[2]: tango,uniform\n depth: 2\n entry_21:\n id: 21\n sku: \"SKU-881747\"\n name: india charlie\n status: active\n score: 99.94\n quantity: 215\n active: false\n updatedAt: \"2026-03-09T12:00:00Z\"\n nested:\n tags[2]: india,delta\n depth: 2\n entry_22:\n id: 22\n sku: \"SKU-536249\"\n name: oscar echo\n status: pending\n score: 49.7\n quantity: 198\n active: true\n updatedAt: \"2026-02-23T12:00:00Z\"\n nested:\n tags[2]: quebec,juliet\n depth: 2\n entry_23:\n id: 23\n sku: \"SKU-242438\"\n name: zulu alpha\n status: failed\n score: 15.35\n quantity: 282\n active: false\n updatedAt: \"2026-01-11T12:00:00Z\"\n nested:\n tags[2]: november,kilo\n depth: 2\n entry_24:\n id: 24\n sku: \"SKU-787964\"\n name: oscar hotel\n status: archived\n score: 35.78\n quantity: 383\n active: true\n updatedAt: \"2026-04-21T12:00:00Z\"\n nested:\n tags[2]: zulu,uniform\n depth: 2\n entry_25:\n id: 25\n sku: \"SKU-270620\"\n name: india india\n status: pending\n score: 30.89\n quantity: 367\n active: false\n updatedAt: \"2026-04-10T12:00:00Z\"\n nested:\n tags[2]: tango,echo\n depth: 2\n entry_26:\n id: 26\n sku: \"SKU-479773\"\n name: whiskey papa\n status: archived\n score: 90.09\n quantity: 10\n active: false\n updatedAt: \"2026-06-16T12:00:00Z\"\n nested:\n tags[2]: uniform,foxtrot\n depth: 2\n entry_27:\n id: 27\n sku: \"SKU-85068\"\n name: kilo zulu\n status: archived\n score: 11.57\n quantity: 100\n active: true\n updatedAt: \"2026-04-01T12:00:00Z\"\n nested:\n tags[2]: uniform,juliet\n depth: 2\n entry_28:\n id: 28\n sku: \"SKU-88619\"\n name: alpha echo\n status: pending\n score: 86.22\n quantity: 23\n active: true\n updatedAt: \"2026-03-02T12:00:00Z\"\n nested:\n tags[2]: romeo,victor\n depth: 2\n entry_29:\n id: 29\n sku: \"SKU-342557\"\n name: bravo papa\n status: failed\n score: 57.84\n quantity: 493\n active: false\n updatedAt: \"2026-05-15T12:00:00Z\"\n nested:\n tags[2]: echo,foxtrot\n depth: 2\n entry_30:\n id: 30\n sku: \"SKU-64537\"\n name: quebec charlie\n status: failed\n score: 65.9\n quantity: 334\n active: false\n updatedAt: \"2026-04-15T12:00:00Z\"\n nested:\n tags[2]: quebec,juliet\n depth: 2\n entry_31:\n id: 31\n sku: \"SKU-45365\"\n name: papa golf\n status: pending\n score: 56.24\n quantity: 315\n active: false\n updatedAt: \"2026-05-07T12:00:00Z\"\n nested:\n tags[2]: bravo,charlie\n depth: 2\n entry_32:\n id: 32\n sku: \"SKU-306983\"\n name: quebec foxtrot\n status: active\n score: 6.96\n quantity: 276\n active: true\n updatedAt: \"2026-02-17T12:00:00Z\"\n nested:\n tags[2]: romeo,victor\n depth: 2\n entry_33:\n id: 33\n sku: \"SKU-100839\"\n name: alpha lima\n status: active\n score: 7.02\n quantity: 308\n active: true\n updatedAt: \"2026-03-01T12:00:00Z\"\n nested:\n tags[2]: uniform,victor\n depth: 2\n entry_34:\n id: 34\n sku: \"SKU-285603\"\n name: quebec echo\n status: pending\n score: 54.61\n quantity: 276\n active: false\n updatedAt: \"2026-04-12T12:00:00Z\"\n nested:\n tags[2]: echo,kilo\n depth: 2\n entry_35:\n id: 35\n sku: \"SKU-129333\"\n name: foxtrot delta\n status: pending\n score: 0.69\n quantity: 36\n active: true\n updatedAt: \"2026-02-11T12:00:00Z\"\n nested:\n tags[2]: november,sierra\n depth: 2\n entry_36:\n id: 36\n sku: \"SKU-704370\"\n name: sierra oscar\n status: active\n score: 45.65\n quantity: 239\n active: false\n updatedAt: \"2026-01-22T12:00:00Z\"\n nested:\n tags[2]: kilo,oscar\n depth: 2\n entry_37:\n id: 37\n sku: \"SKU-810963\"\n name: foxtrot alpha\n status: failed\n score: 59.48\n quantity: 48\n active: true\n updatedAt: \"2026-06-24T12:00:00Z\"\n nested:\n tags[2]: juliet,zulu\n depth: 2\n entry_38:\n id: 38\n sku: \"SKU-171741\"\n name: zulu uniform\n status: failed\n score: 42.96\n quantity: 48\n active: true\n updatedAt: \"2026-03-22T12:00:00Z\"\n nested:\n tags[2]: delta,charlie\n depth: 2\n entry_39:\n id: 39\n sku: \"SKU-893779\"\n name: alpha tango\n status: archived\n score: 0.81\n quantity: 390\n active: false\n updatedAt: \"2026-06-25T12:00:00Z\"\n nested:\n tags[2]: echo,victor\n depth: 2\n entry_40:\n id: 40\n sku: \"SKU-455048\"\n name: quebec uniform\n status: archived\n score: 52.81\n quantity: 296\n active: true\n updatedAt: \"2026-03-11T12:00:00Z\"\n nested:\n tags[2]: quebec,kilo\n depth: 2\n entry_41:\n id: 41\n sku: \"SKU-579597\"\n name: alpha bravo\n status: active\n score: 74.63\n quantity: 209\n active: false\n updatedAt: \"2026-02-05T12:00:00Z\"\n nested:\n tags[2]: papa,charlie\n depth: 2\n entry_42:\n id: 42\n sku: \"SKU-927995\"\n name: bravo oscar\n status: failed\n score: 46.03\n quantity: 13\n active: true\n updatedAt: \"2026-01-18T12:00:00Z\"\n nested:\n tags[2]: romeo,uniform\n depth: 2\n entry_43:\n id: 43\n sku: \"SKU-640313\"\n name: oscar foxtrot\n status: failed\n score: 75.06\n quantity: 102\n active: true\n updatedAt: \"2026-06-10T12:00:00Z\"\n nested:\n tags[2]: november,quebec\n depth: 2\n entry_44:\n id: 44\n sku: \"SKU-587339\"\n name: delta lima\n status: pending\n score: 1.02\n quantity: 160\n active: false\n updatedAt: \"2026-03-12T12:00:00Z\"\n nested:\n tags[2]: kilo,november\n depth: 2\n entry_45:\n id: 45\n sku: \"SKU-22977\"\n name: alpha charlie\n status: active\n score: 91.65\n quantity: 457\n active: false\n updatedAt: \"2026-02-26T12:00:00Z\"\n nested:\n tags[2]: india,kilo\n depth: 2\n entry_46:\n id: 46\n sku: \"SKU-274156\"\n name: foxtrot oscar\n status: archived\n score: 31.37\n quantity: 424\n active: true\n updatedAt: \"2026-01-24T12:00:00Z\"\n nested:\n tags[2]: alpha,foxtrot\n depth: 2\n entry_47:\n id: 47\n sku: \"SKU-533189\"\n name: golf lima\n status: active\n score: 9.71\n quantity: 251\n active: false\n updatedAt: \"2026-04-01T12:00:00Z\"\n nested:\n tags[2]: india,kilo\n depth: 2\n entry_48:\n id: 48\n sku: \"SKU-290356\"\n name: zulu zulu\n status: archived\n score: 25.89\n quantity: 432\n active: false\n updatedAt: \"2026-04-14T12:00:00Z\"\n nested:\n tags[2]: alpha,kilo\n depth: 2\n entry_49:\n id: 49\n sku: \"SKU-746759\"\n name: juliet tango\n status: active\n score: 40.83\n quantity: 374\n active: false\n updatedAt: \"2026-06-22T12:00:00Z\"\n nested:\n tags[2]: delta,golf\n depth: 2" + } + }, + { + "name": "boundary-obj-1", + "rawContent": "{\"key_0\":\"val, with comma 0\"}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\"}", + "encoded": "key_0: \"val, with comma 0\"" + } + }, + { + "name": "boundary-obj-2", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5" + } + }, + { + "name": "boundary-obj-3", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\"}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\"}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"" + } + }, + { + "name": "boundary-obj-4", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5" + } + }, + { + "name": "boundary-obj-5", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\"}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\"}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"" + } + }, + { + "name": "boundary-obj-6", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"\nkey_5: 7.5" + } + }, + { + "name": "boundary-obj-7", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\"}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\"}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"\nkey_5: 7.5\nkey_6: \"val, with comma 6\"" + } + }, + { + "name": "boundary-obj-8", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"\nkey_5: 7.5\nkey_6: \"val, with comma 6\"\nkey_7: 10.5" + } + }, + { + "name": "boundary-obj-9", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5,\"key_8\":\"val, with comma 8\"}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5,\"key_8\":\"val, with comma 8\"}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"\nkey_5: 7.5\nkey_6: \"val, with comma 6\"\nkey_7: 10.5\nkey_8: \"val, with comma 8\"" + } + }, + { + "name": "boundary-obj-10", + "rawContent": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5,\"key_8\":\"val, with comma 8\",\"key_9\":13.5}", + "unwrap": true, + "expected": { + "normalized": "{\"key_0\":\"val, with comma 0\",\"key_1\":1.5,\"key_2\":\"val, with comma 2\",\"key_3\":4.5,\"key_4\":\"val, with comma 4\",\"key_5\":7.5,\"key_6\":\"val, with comma 6\",\"key_7\":10.5,\"key_8\":\"val, with comma 8\",\"key_9\":13.5}", + "encoded": "key_0: \"val, with comma 0\"\nkey_1: 1.5\nkey_2: \"val, with comma 2\"\nkey_3: 4.5\nkey_4: \"val, with comma 4\"\nkey_5: 7.5\nkey_6: \"val, with comma 6\"\nkey_7: 10.5\nkey_8: \"val, with comma 8\"\nkey_9: 13.5" + } + }, + { + "name": "boundary-hetero-arr-1", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0}]", + "encoded": "[1]{f0,n}:\n \"x,y 0\",0" + } + }, + { + "name": "boundary-hetero-arr-2", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1}]", + "encoded": "[2]:\n - f0: \"x,y 0\"\n n: 0\n - f1: \"x,y 1\"\n n: 1" + } + }, + { + "name": "boundary-hetero-arr-3", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2}]", + "encoded": "[3]:\n - f0: \"x,y 0\"\n n: 0\n - f1: \"x,y 1\"\n n: 1\n - f2: \"x,y 2\"\n n: 2" + } + }, + { + "name": "boundary-hetero-arr-4", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3}]", + "encoded": "[4]:\n - f0: \"x,y 0\"\n n: 0\n - f1: \"x,y 1\"\n n: 1\n - f2: \"x,y 2\"\n n: 2\n - f3: \"x,y 3\"\n n: 3" + } + }, + { + "name": "boundary-hetero-arr-5", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3},{\"f4\":\"x,y 4\",\"n\":4}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3},{\"f4\":\"x,y 4\",\"n\":4}]", + "encoded": "[5]:\n - f0: \"x,y 0\"\n n: 0\n - f1: \"x,y 1\"\n n: 1\n - f2: \"x,y 2\"\n n: 2\n - f3: \"x,y 3\"\n n: 3\n - f4: \"x,y 4\"\n n: 4" + } + }, + { + "name": "boundary-hetero-arr-6", + "rawContent": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3},{\"f4\":\"x,y 4\",\"n\":4},{\"f5\":\"x,y 5\",\"n\":5}]", + "unwrap": true, + "expected": { + "normalized": "[{\"f0\":\"x,y 0\",\"n\":0},{\"f1\":\"x,y 1\",\"n\":1},{\"f2\":\"x,y 2\",\"n\":2},{\"f3\":\"x,y 3\",\"n\":3},{\"f4\":\"x,y 4\",\"n\":4},{\"f5\":\"x,y 5\",\"n\":5}]", + "encoded": "[6]:\n - f0: \"x,y 0\"\n n: 0\n - f1: \"x,y 1\"\n n: 1\n - f2: \"x,y 2\"\n n: 2\n - f3: \"x,y 3\"\n n: 3\n - f4: \"x,y 4\"\n n: 4\n - f5: \"x,y 5\"\n n: 5" + } + }, + { + "name": "boundary-hyphen-arr-1", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0}]", + "encoded": "[1]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0" + } + }, + { + "name": "boundary-hyphen-arr-2", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1}]", + "encoded": "[2]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1" + } + }, + { + "name": "boundary-hyphen-arr-3", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2}]", + "encoded": "[3]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2" + } + }, + { + "name": "boundary-hyphen-arr-4", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3}]", + "encoded": "[4]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2\n \"AB-103\",\"us-east-3\",3" + } + }, + { + "name": "boundary-hyphen-arr-5", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4}]", + "encoded": "[5]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2\n \"AB-103\",\"us-east-3\",3\n \"AB-104\",\"us-east-4\",4" + } + }, + { + "name": "boundary-hyphen-arr-6", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5}]", + "encoded": "[6]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2\n \"AB-103\",\"us-east-3\",3\n \"AB-104\",\"us-east-4\",4\n \"AB-105\",\"us-east-5\",5" + } + }, + { + "name": "boundary-hyphen-arr-7", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5},{\"sku\":\"AB-106\",\"zone\":\"us-east-6\",\"n\":6}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5},{\"sku\":\"AB-106\",\"zone\":\"us-east-6\",\"n\":6}]", + "encoded": "[7]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2\n \"AB-103\",\"us-east-3\",3\n \"AB-104\",\"us-east-4\",4\n \"AB-105\",\"us-east-5\",5\n \"AB-106\",\"us-east-6\",6" + } + }, + { + "name": "boundary-hyphen-arr-8", + "rawContent": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5},{\"sku\":\"AB-106\",\"zone\":\"us-east-6\",\"n\":6},{\"sku\":\"AB-107\",\"zone\":\"us-east-7\",\"n\":7}]", + "unwrap": true, + "expected": { + "normalized": "[{\"sku\":\"AB-100\",\"zone\":\"us-east-0\",\"n\":0},{\"sku\":\"AB-101\",\"zone\":\"us-east-1\",\"n\":1},{\"sku\":\"AB-102\",\"zone\":\"us-east-2\",\"n\":2},{\"sku\":\"AB-103\",\"zone\":\"us-east-3\",\"n\":3},{\"sku\":\"AB-104\",\"zone\":\"us-east-4\",\"n\":4},{\"sku\":\"AB-105\",\"zone\":\"us-east-5\",\"n\":5},{\"sku\":\"AB-106\",\"zone\":\"us-east-6\",\"n\":6},{\"sku\":\"AB-107\",\"zone\":\"us-east-7\",\"n\":7}]", + "encoded": "[8]{sku,zone,n}:\n \"AB-100\",\"us-east-0\",0\n \"AB-101\",\"us-east-1\",1\n \"AB-102\",\"us-east-2\",2\n \"AB-103\",\"us-east-3\",3\n \"AB-104\",\"us-east-4\",4\n \"AB-105\",\"us-east-5\",5\n \"AB-106\",\"us-east-6\",6\n \"AB-107\",\"us-east-7\",7" + } + }, + { + "name": "fine-r1-b0-h1", + "rawContent": "[{\"h0\":\"us-east-00\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\"}]", + "encoded": "[1]{h0}:\n \"us-east-00\"" + } + }, + { + "name": "fine-r2-b0-h1", + "rawContent": "[{\"h0\":\"us-east-00\"},{\"h0\":\"us-east-10\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\"},{\"h0\":\"us-east-10\"}]", + "encoded": "[2]{h0}:\n \"us-east-00\"\n \"us-east-10\"" + } + }, + { + "name": "fine-r1-b0-h2", + "rawContent": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "encoded": "[1]{h0,h1}:\n \"us-east-00\",\"us-east-01\"" + } + }, + { + "name": "fine-r2-b0-h2", + "rawContent": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "encoded": "[2]{h0,h1}:\n \"us-east-00\",\"us-east-01\"\n \"us-east-10\",\"us-east-11\"" + } + }, + { + "name": "fine-r1-b0-h3", + "rawContent": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "encoded": "[1]{h0,h1,h2}:\n \"us-east-00\",\"us-east-01\",\"us-east-02\"" + } + }, + { + "name": "fine-r2-b0-h3", + "rawContent": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "encoded": "[2]{h0,h1,h2}:\n \"us-east-00\",\"us-east-01\",\"us-east-02\"\n \"us-east-10\",\"us-east-11\",\"us-east-12\"" + } + }, + { + "name": "fine-r1-b1-h1", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\"}]", + "encoded": "[1]{b0,h0}:\n plain00,\"us-east-00\"" + } + }, + { + "name": "fine-r2-b1-h1", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\"}]", + "encoded": "[2]{b0,h0}:\n plain00,\"us-east-00\"\n plain10,\"us-east-10\"" + } + }, + { + "name": "fine-r1-b1-h2", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "encoded": "[1]{b0,h0,h1}:\n plain00,\"us-east-00\",\"us-east-01\"" + } + }, + { + "name": "fine-r2-b1-h2", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "encoded": "[2]{b0,h0,h1}:\n plain00,\"us-east-00\",\"us-east-01\"\n plain10,\"us-east-10\",\"us-east-11\"" + } + }, + { + "name": "fine-r1-b1-h3", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "encoded": "[1]{b0,h0,h1,h2}:\n plain00,\"us-east-00\",\"us-east-01\",\"us-east-02\"" + } + }, + { + "name": "fine-r2-b1-h3", + "rawContent": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "encoded": "[2]{b0,h0,h1,h2}:\n plain00,\"us-east-00\",\"us-east-01\",\"us-east-02\"\n plain10,\"us-east-10\",\"us-east-11\",\"us-east-12\"" + } + }, + { + "name": "fine-r1-b2-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\"}]", + "encoded": "[1]{b0,b1,h0}:\n plain00,plain01,\"us-east-00\"" + } + }, + { + "name": "fine-r2-b2-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\"}]", + "encoded": "[2]{b0,b1,h0}:\n plain00,plain01,\"us-east-00\"\n plain10,plain11,\"us-east-10\"" + } + }, + { + "name": "fine-r1-b2-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "encoded": "[1]{b0,b1,h0,h1}:\n plain00,plain01,\"us-east-00\",\"us-east-01\"" + } + }, + { + "name": "fine-r2-b2-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "encoded": "[2]{b0,b1,h0,h1}:\n plain00,plain01,\"us-east-00\",\"us-east-01\"\n plain10,plain11,\"us-east-10\",\"us-east-11\"" + } + }, + { + "name": "fine-r1-b2-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "encoded": "[1]{b0,b1,h0,h1,h2}:\n plain00,plain01,\"us-east-00\",\"us-east-01\",\"us-east-02\"" + } + }, + { + "name": "fine-r2-b2-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "encoded": "[2]{b0,b1,h0,h1,h2}:\n plain00,plain01,\"us-east-00\",\"us-east-01\",\"us-east-02\"\n plain10,plain11,\"us-east-10\",\"us-east-11\",\"us-east-12\"" + } + }, + { + "name": "fine-r1-b3-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\"}]", + "encoded": "[1]{b0,b1,b2,h0}:\n plain00,plain01,plain02,\"us-east-00\"" + } + }, + { + "name": "fine-r2-b3-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\"}]", + "encoded": "[2]{b0,b1,b2,h0}:\n plain00,plain01,plain02,\"us-east-00\"\n plain10,plain11,plain12,\"us-east-10\"" + } + }, + { + "name": "fine-r1-b3-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "encoded": "[1]{b0,b1,b2,h0,h1}:\n plain00,plain01,plain02,\"us-east-00\",\"us-east-01\"" + } + }, + { + "name": "fine-r2-b3-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "encoded": "[2]{b0,b1,b2,h0,h1}:\n plain00,plain01,plain02,\"us-east-00\",\"us-east-01\"\n plain10,plain11,plain12,\"us-east-10\",\"us-east-11\"" + } + }, + { + "name": "fine-r1-b3-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "encoded": "[1]{b0,b1,b2,h0,h1,h2}:\n plain00,plain01,plain02,\"us-east-00\",\"us-east-01\",\"us-east-02\"" + } + }, + { + "name": "fine-r2-b3-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "encoded": "[2]{b0,b1,b2,h0,h1,h2}:\n plain00,plain01,plain02,\"us-east-00\",\"us-east-01\",\"us-east-02\"\n plain10,plain11,plain12,\"us-east-10\",\"us-east-11\",\"us-east-12\"" + } + }, + { + "name": "fine-r1-b4-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\"}]", + "encoded": "[1]{b0,b1,b2,b3,h0}:\n plain00,plain01,plain02,plain03,\"us-east-00\"" + } + }, + { + "name": "fine-r2-b4-h1", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\"}]", + "encoded": "[2]{b0,b1,b2,b3,h0}:\n plain00,plain01,plain02,plain03,\"us-east-00\"\n plain10,plain11,plain12,plain13,\"us-east-10\"" + } + }, + { + "name": "fine-r1-b4-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"}]", + "encoded": "[1]{b0,b1,b2,b3,h0,h1}:\n plain00,plain01,plain02,plain03,\"us-east-00\",\"us-east-01\"" + } + }, + { + "name": "fine-r2-b4-h2", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\"}]", + "encoded": "[2]{b0,b1,b2,b3,h0,h1}:\n plain00,plain01,plain02,plain03,\"us-east-00\",\"us-east-01\"\n plain10,plain11,plain12,plain13,\"us-east-10\",\"us-east-11\"" + } + }, + { + "name": "fine-r1-b4-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"}]", + "encoded": "[1]{b0,b1,b2,b3,h0,h1,h2}:\n plain00,plain01,plain02,plain03,\"us-east-00\",\"us-east-01\",\"us-east-02\"" + } + }, + { + "name": "fine-r2-b4-h3", + "rawContent": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "unwrap": true, + "expected": { + "normalized": "[{\"b0\":\"plain00\",\"b1\":\"plain01\",\"b2\":\"plain02\",\"b3\":\"plain03\",\"h0\":\"us-east-00\",\"h1\":\"us-east-01\",\"h2\":\"us-east-02\"},{\"b0\":\"plain10\",\"b1\":\"plain11\",\"b2\":\"plain12\",\"b3\":\"plain13\",\"h0\":\"us-east-10\",\"h1\":\"us-east-11\",\"h2\":\"us-east-12\"}]", + "encoded": "[2]{b0,b1,b2,b3,h0,h1,h2}:\n plain00,plain01,plain02,plain03,\"us-east-00\",\"us-east-01\",\"us-east-02\"\n plain10,plain11,plain12,plain13,\"us-east-10\",\"us-east-11\",\"us-east-12\"" + } + } +] diff --git a/platform/archestra-rs/proxy-transform-core/tests/golden_corpus.rs b/platform/archestra-rs/proxy-transform-core/tests/golden_corpus.rs new file mode 100644 index 00000000000..41fbfe7b7d9 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/tests/golden_corpus.rs @@ -0,0 +1,101 @@ +//! Golden-corpus conformance: every fixture input must produce exactly the +//! committed `normalized` + `encoded` output. The goldens pin the TOON spec-v3 +//! encoding the proxy ships (decision: migrated wire format, `toon-format` crate +//! is the source of truth — there is no npm oracle). +//! +//! Regenerating `tests/fixtures/golden-corpus.json`: +//! +//! 1. Inputs (only when the corpus itself changes) — from `platform/backend`: +//! `pnpm exec tsx ../archestra-rs/proxy-transform-core/tests/fixtures/gen-corpus.mts` +//! 2. Expected outputs — from `platform/archestra-rs`: +//! `UPDATE_TOON_GOLDENS=1 cargo test -p proxy_transform_core --test golden_corpus` +//! +//! Review the diff: a changed golden is a wire-format change. + +use proxy_transform_core::{ToonEncodeItem, ToonEncodeResult, toon_encode_tool_results}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct GoldenCase { + name: String, + raw_content: String, + unwrap: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + expected: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExpectedOutput { + normalized: String, + encoded: Option, +} + +fn fixture_path() -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/golden-corpus.json") +} + +/// Update deliberately when the corpus generator changes — this pins fixture +/// truncation from silently shrinking coverage. +const EXPECTED_CASE_COUNT: usize = 120; + +#[test] +fn golden_corpus_conformance() { + let path = fixture_path(); + let raw = std::fs::read_to_string(&path).expect("read golden corpus fixture"); + let mut cases: Vec = serde_json::from_str(&raw).expect("parse golden corpus"); + assert_eq!( + cases.len(), + EXPECTED_CASE_COUNT, + "golden corpus size changed; update EXPECTED_CASE_COUNT deliberately" + ); + + let items: Vec = cases + .iter() + .map(|case| ToonEncodeItem { + id: case.name.clone(), + raw_content: case.raw_content.clone(), + unwrap: case.unwrap, + }) + .collect(); + let results: Vec = toon_encode_tool_results(items, None); + assert_eq!(results.len(), cases.len(), "positional contract"); + + if std::env::var("UPDATE_TOON_GOLDENS").as_deref() == Ok("1") { + assert!( + std::env::var_os("CI").is_none(), + "refusing to regenerate goldens in CI" + ); + for (case, result) in cases.iter_mut().zip(&results) { + case.expected = Some(ExpectedOutput { + normalized: result.normalized.clone(), + encoded: result.encoded.clone(), + }); + } + let mut out = serde_json::to_string_pretty(&cases).expect("serialize goldens"); + out.push('\n'); + std::fs::write(&path, out).expect("write golden corpus fixture"); + // Fall through: the freshly written goldens must pass the comparison. + } + + for (case, result) in cases.iter().zip(&results) { + let expected = case.expected.as_ref().unwrap_or_else(|| { + panic!( + "golden case {:?} has no expected output; regenerate with \ + UPDATE_TOON_GOLDENS=1 cargo test -p proxy_transform_core --test golden_corpus", + case.name + ) + }); + assert_eq!( + result.normalized, expected.normalized, + "normalized mismatch for {:?}", + case.name + ); + assert_eq!( + result.encoded, expected.encoded, + "encoded mismatch for {:?}", + case.name + ); + } +} diff --git a/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.proptest-regressions b/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.proptest-regressions new file mode 100644 index 00000000000..6f90200da61 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 664598e1d33a784d747ecd9467351725d66897eab37abbc66158e5cb22da4f8d # shrinks to value = String("0¡") diff --git a/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.rs b/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.rs new file mode 100644 index 00000000000..787dfed8abe --- /dev/null +++ b/platform/archestra-rs/proxy-transform-core/tests/roundtrip_property.rs @@ -0,0 +1,270 @@ +//! Round-trip property tests: for generated JSON documents, +//! `decode(encode(parse(raw)))` is semantically equal to `parse(raw)`. The +//! goldens pin regression; this pins encoder/decoder semantics on shapes we did +//! not enumerate by hand. The comparison baseline is the PARSED value, not the +//! generated one: serde_json's default float parse can be 1 ULP off the +//! shortest-repr literal (a disclosed JS→Rust migration difference), and that +//! parse step is part of the kernel under test, not of TOON. +//! +//! Generator constraints mirror known `toon-format` 0.5 quirks (verified in the +//! validation spike) so we do not pin upstream bugs as our own failures: +//! - no empty objects inside containers: an array of all-empty objects encodes +//! as `[N]{}:`, which the crate's own decoder rejects (upstream issue #74); +//! - numbers stay well inside +/- 2^63 and floats moderate: integer-valued +//! floats around 2^64 and above decode back as strings; +//! - object keys containing `.` are restricted to plain dotted identifiers +//! (which the encoder emits unquoted): the decoder prepends a spurious NUL to +//! QUOTED keys containing dots (e.g. `{"a":null,"...":null}` decodes the +//! second key as `"\0..."`, and `"𝄞."`/nested `"¡."` fail the same way); +//! - in arrays that hold container elements (list layout), string elements get +//! whitespace replaced and leading digits prefixed: the encoder emits such +//! strings unquoted and its own decoder cannot re-parse the layout (e.g. +//! `["a b",[]]` and `[[],"0a"]` both fail to decode); +//! - object keys are non-empty: `[{"":null}]` encodes to a tabular header +//! `[1]{""}:` that the decoder rejects ("Field name cannot be empty"); +//! - string content limits control/whitespace characters to `\n`, `\t` and +//! space: exotic ones are emitted raw in unquoted positions and lost on +//! decode (e.g. a vertical tab in `"A\u{b} x"` decodes as `"A x"`); +//! - a ROOT-level document that is a bare digit-leading string gets a space +//! inserted after the digit run by the decoder (`"0¡"` decodes as `"0 ¡"`); +//! container positions are unaffected, so only the root string is dodged. + +use proptest::prelude::*; +use proxy_transform_core::{ToonEncodeItem, toon_encode_tool_results}; +use serde_json::Value; + +fn arb_key() -> impl Strategy { + prop_oneof![ + // Any non-empty key without a dot (see module docs for the quirks). + "[^.]+".prop_map(neutralize_exotic_whitespace), + // Dotted identifier keys, always emitted unquoted — these round-trip. + "[a-z][a-z0-9_]{0,5}(\\.[a-z][a-z0-9_]{0,5}){1,2}", + ] +} + +fn arb_string() -> impl Strategy { + ".*".prop_map(neutralize_exotic_whitespace) +} + +/// See module docs: keep `\n`, `\t` and space (verified to round-trip), map +/// other control/whitespace characters to `_`. +fn neutralize_exotic_whitespace(text: String) -> String { + text.chars() + .map(|c| match c { + '\n' | '\t' | ' ' => c, + c if c.is_control() || c.is_whitespace() => '_', + c => c, + }) + .collect() +} + +fn arb_json() -> impl Strategy { + let leaf = prop_oneof![ + Just(Value::Null), + any::().prop_map(Value::Bool), + (-(1i64 << 62)..(1i64 << 62)).prop_map(|n| Value::Number(n.into())), + (-1.0e15..1.0e15f64).prop_map(|f| { + serde_json::Number::from_f64(f) + .map(Value::Number) + .unwrap_or(Value::Null) + }), + arb_string().prop_map(Value::String), + ]; + leaf.prop_recursive(4, 48, 8, |inner| { + prop_oneof![ + prop::collection::vec(inner.clone(), 0..8).prop_map(Value::Array), + // Objects always carry at least one key (see module docs). + prop::collection::btree_map(arb_key(), inner, 1..8) + .prop_map(|map| Value::Object(map.into_iter().collect())), + ] + }) + .prop_map(dodge_list_layout_quirk) + .prop_map(dodge_root_digit_string_quirk) +} + +/// See module docs: only a root-level digit-leading string trips the decoder's +/// digit-run split, so prefix it the same way the list-layout dodge does. +fn dodge_root_digit_string_quirk(value: Value) -> Value { + match value { + Value::String(text) if text.starts_with(|c: char| c.is_ascii_digit()) => { + Value::String(format!("_{text}")) + } + other => other, + } +} + +/// Unconstrained JSON generator for encode-only properties: no decoder-quirk +/// exclusions (any strings/keys incl. control chars and dots, empty objects, +/// full i64/f64 ranges) — the wrapper property never decodes TOON. +fn arb_json_encode_only() -> impl Strategy { + let leaf = prop_oneof![ + Just(Value::Null), + any::().prop_map(Value::Bool), + any::().prop_map(|n| Value::Number(n.into())), + any::().prop_map(|f| { + serde_json::Number::from_f64(f) + .map(Value::Number) + .unwrap_or(Value::Null) + }), + ".*".prop_map(Value::String), + ]; + leaf.prop_recursive(4, 48, 8, |inner| { + prop_oneof![ + prop::collection::vec(inner.clone(), 0..8).prop_map(Value::Array), + prop::collection::btree_map(".*", inner, 0..8) + .prop_map(|map| Value::Object(map.into_iter().collect())), + ] + }) +} + +/// See module docs: keep generated shapes rich, but neutralize string elements +/// of arrays that also hold containers (whitespace, leading digits) so we do +/// not pin the encoder's unquoted-list-string / decoder disagreement as our +/// failure. +fn dodge_list_layout_quirk(value: Value) -> Value { + match value { + Value::Array(items) => { + let has_container = items.iter().any(|item| item.is_array() || item.is_object()); + let items = items + .into_iter() + .map(|item| match item { + Value::String(text) if has_container => { + let mut text = text.replace(char::is_whitespace, "_"); + if text.starts_with(|c: char| c.is_ascii_digit()) { + text.insert(0, '_'); + } + Value::String(text) + } + other => dodge_list_layout_quirk(other), + }) + .collect(); + Value::Array(items) + } + Value::Object(map) => Value::Object( + map.into_iter() + .map(|(key, item)| (key, dodge_list_layout_quirk(item))) + .collect(), + ), + other => other, + } +} + +/// Numeric-tolerant equality: i64/u64/f64 representations of the same number +/// compare equal (the decoder may pick a different `serde_json::Number` repr). +/// Integers are compared exactly (an f64 detour would collapse distinct +/// integers above 2^53); the f64 fallback only applies when a side is a float. +fn semantically_equal(a: &Value, b: &Value) -> bool { + match (a, b) { + (Value::Number(x), Value::Number(y)) => { + if let (Some(ix), Some(iy)) = (x.as_i64(), y.as_i64()) { + ix == iy + } else if let (Some(ux), Some(uy)) = (x.as_u64(), y.as_u64()) { + ux == uy + } else if let (Some(fx), Some(fy)) = (x.as_f64(), y.as_f64()) { + fx == fy + } else { + x == y + } + } + (Value::Array(xs), Value::Array(ys)) => { + xs.len() == ys.len() + && xs + .iter() + .zip(ys.iter()) + .all(|(x, y)| semantically_equal(x, y)) + } + (Value::Object(xs), Value::Object(ys)) => { + xs.len() == ys.len() + && xs + .iter() + .all(|(key, x)| ys.get(key).is_some_and(|y| semantically_equal(x, y))) + } + _ => a == b, + } +} + +proptest! { + /// Differential parity oracle: our encoder must produce byte-identical + /// output to `toon_format::encode_default` for any parseable JSON. Uses + /// the unconstrained generator — encode-only, so no decoder-quirk + /// exclusions apply. The allowed divergences are the deliberate + /// anti-amplification limits: when our encoder returns `None`, either the + /// input exceeds the per-item cap or the crate's output must genuinely + /// exceed `max(2 x input bytes, 16KiB)`. (The aggregate batch budget never + /// binds here: a single-item batch is gated before any output exists.) + #[test] + fn encoder_matches_toon_format_crate(value in arb_json_encode_only()) { + let raw = serde_json::to_string(&value).expect("serialize generated value"); + let parsed: Value = serde_json::from_str(&raw).expect("reparse generated document"); + let raw_len = raw.len(); + let budget = (2 * raw_len).max(16 * 1024); + let results = toon_encode_tool_results( + vec![ToonEncodeItem { + id: "diff".to_string(), + raw_content: raw, + unwrap: false, + }], + None, + ); + let expected = toon_format::encode_default(&parsed).ok(); + match (&results[0].encoded, &expected) { + (None, Some(crate_output)) => prop_assert!( + raw_len > proxy_transform_core::MAX_ITEM_INPUT_BYTES + || crate_output.len() > budget, + "our encoder returned None but no skip condition held (input {} bytes, \ + crate output {} bytes, budget {}) for value: {}", + raw_len, + crate_output.len(), + budget, + parsed + ), + (ours, expected) => prop_assert_eq!( + ours, + expected, + "encoder diverged from toon-format crate for value: {}", + parsed + ), + } + } + + #[test] + fn encode_decode_roundtrips(value in arb_json()) { + let raw = serde_json::to_string(&value).expect("serialize generated value"); + let parsed: Value = serde_json::from_str(&raw).expect("reparse generated document"); + let results = toon_encode_tool_results( + vec![ToonEncodeItem { + id: "prop".to_string(), + raw_content: raw, + unwrap: false, + }], + None, + ); + let encoded = results[0].encoded.as_ref().expect("valid JSON always encodes"); + let decoded: Value = toon_format::decode_default(encoded) + .unwrap_or_else(|error| panic!("decode failed: {error}\nencoded:\n{encoded}")); + prop_assert!( + semantically_equal(&parsed, &decoded), + "round-trip mismatch:\nparsed: {parsed}\ndecoded: {decoded}\nencoded:\n{encoded}" + ); + } + + /// Unwrapping the `[{"type":"text","text":...}]` wrapper must yield exactly + /// the encoding of the inner payload. + #[test] + fn wrapped_input_encodes_like_inner_payload(value in arb_json_encode_only()) { + let inner = serde_json::to_string(&value).expect("serialize generated value"); + let wrapped = serde_json::to_string(&serde_json::json!([ + {"type": "text", "text": inner} + ])) + .expect("serialize wrapper"); + let results = toon_encode_tool_results( + vec![ + ToonEncodeItem { id: "direct".to_string(), raw_content: inner.clone(), unwrap: false }, + ToonEncodeItem { id: "wrapped".to_string(), raw_content: wrapped, unwrap: true }, + ], + None, + ); + prop_assert_eq!(&results[1].normalized, &inner); + prop_assert_eq!(&results[1].encoded, &results[0].encoded); + } +} diff --git a/platform/archestra-rs/proxy-transform-rs/Cargo.toml b/platform/archestra-rs/proxy-transform-rs/Cargo.toml new file mode 100644 index 00000000000..d6abdd26ba1 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "proxy_transform_rs" +version = "0.1.0" +edition = "2024" +build = "build.rs" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = "3" +napi-derive = "3" +proxy_transform_core = { path = "../proxy-transform-core", features = ["napi"] } +serde_json = "1" + +[build-dependencies] +napi-build = "2" diff --git a/platform/archestra-rs/proxy-transform-rs/build.rs b/platform/archestra-rs/proxy-transform-rs/build.rs new file mode 100644 index 00000000000..0f1b01002b0 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/platform/archestra-rs/proxy-transform-rs/index.cjs b/platform/archestra-rs/proxy-transform-rs/index.cjs new file mode 100644 index 00000000000..94b5b52024c --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/index.cjs @@ -0,0 +1,16 @@ +"use strict"; + +const { loadNativeBinding, wrapAsync } = require("@archestra/napi-loader"); + +const nativeBinding = loadNativeBinding({ + dir: __dirname, + crateName: "proxy_transform_rs", + packageName: "@archestra/proxy-transform-rs", +}); + +// explicit per-name assignment so Node's cjs-module-lexer exposes each as a +// named ESM export (consumers do `import { toonEncodeToolResults } from ...`) +module.exports.toonEncodeToolResults = wrapAsync( + nativeBinding, + "toonEncodeToolResults", +); diff --git a/platform/archestra-rs/proxy-transform-rs/index.d.ts b/platform/archestra-rs/proxy-transform-rs/index.d.ts new file mode 100644 index 00000000000..f21dab47613 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/index.d.ts @@ -0,0 +1,67 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +/** + * Which string an adapter tokenizes as the pre-compression baseline, selecting + * the fused token counting (see [`toon_encode_tool_results`]). Passing `None` + * there skips counting entirely (Anthropic and Bedrock keep their own JS + * tokenizer); the two variants below cover the tiktoken-family adapters. + */ +export declare const enum BeforeSource { + /** + * Count the original `raw_content` (pre-unwrap) as the baseline — Gemini, + * which tokenizes its serialized response, not the unwrapped inner text. + */ + Raw = 'Raw', + /** + * Count the `normalized` (post-unwrap) content — every other tiktoken + * adapter (OpenAI family, Cohere, ZhipuAI, MiniMax). + */ + Normalized = 'Normalized' +} + +/** + * One tool result to transform. `id` is the provider tool id, carried for + * logging only — it is not unique across items (Anthropic reuses one + * `tool_use_id` across blocks), so results are matched to inputs by position. + */ +export interface ToonEncodeItem { + id: string + rawContent: string + unwrap: boolean +} + +/** + * The transformation output for one item. `normalized` is the unwrapped string + * when unwrapping was requested and matched, else the original `raw_content` + * (adapters tokenize it for accounting). `encoded` is the TOON encoding, or + * `None` when the content is not parseable JSON — or when encoding would + * exceed the anti-amplification output budget (see `encode` module docs); + * adapters keep the original payload either way. + * + * `before_tokens`/`encoded_tokens` are the cl100k counts of the baseline and + * the compressed candidate, populated only when a [`BeforeSource`] is passed + * AND `encoded` is `Some` (an unencodable item is never tokenized, matching + * the adapters). Both `None` otherwise. + * + * `use_nullable` makes `None` fields cross the boundary as an explicit JS + * `null` (typed `string | null` / `number | null`) instead of an omitted key. + */ +export interface ToonEncodeResult { + normalized: string + encoded: string | null + beforeTokens: number | null + encodedTokens: number | null +} +/** + * Transform a batch of tool results off the JS thread: optionally unwrap the + * `[{"type":"text","text":...}]` client wrapper, parse the JSON, and encode it + * as TOON (spec v3). Results are positional — same length and order as + * `items`; content that is not parseable JSON yields `encoded: null` (the + * caller keeps the original payload). + * + * `beforeSource` selects the fused cl100k token counting: pass it to have the + * off-thread pass populate `beforeTokens`/`encodedTokens` (`Normalized` for + * most adapters, `Raw` for Gemini), or omit it to skip counting (Anthropic and + * Bedrock tokenize with their own tokenizer). + */ +export declare function toonEncodeToolResults(items: Array, beforeSource?: BeforeSource | undefined | null): Promise> diff --git a/platform/archestra-rs/proxy-transform-rs/package.json b/platform/archestra-rs/proxy-transform-rs/package.json new file mode 100644 index 00000000000..71f97291da5 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/package.json @@ -0,0 +1,30 @@ +{ + "name": "@archestra/proxy-transform-rs", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "main": "index.cjs", + "types": "index.d.ts", + "scripts": { + "build": "napi build --release --platform", + "build:dev": "napi build --profile release-fast --platform", + "check:ci": "cargo fmt --check --all && cargo check -p proxy_transform_core -p proxy_transform_rs --locked && cargo test -p proxy_transform_core --locked && cargo test -p proxy_transform_core --features napi --locked && pnpm build && pnpm smoke && pnpm smoke:esm", + "check:musl": "cargo test -p proxy_transform_core --locked && pnpm build && pnpm smoke && pnpm smoke:esm", + "lint": "cargo fmt --check --all", + "smoke": "node smoke.test.cjs", + "smoke:esm": "node smoke.esm.test.mjs", + "test": "cargo test -p proxy_transform_core --locked", + "type-check": "cargo check -p proxy_transform_core -p proxy_transform_rs --locked" + }, + "dependencies": { + "@archestra/napi-loader": "workspace:*" + }, + "devDependencies": { + "@napi-rs/cli": "^3.4.0" + }, + "files": [ + "index.cjs", + "index.d.ts", + "*.node" + ] +} diff --git a/platform/archestra-rs/proxy-transform-rs/smoke.esm.test.mjs b/platform/archestra-rs/proxy-transform-rs/smoke.esm.test.mjs new file mode 100644 index 00000000000..c378b10fb57 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/smoke.esm.test.mjs @@ -0,0 +1,21 @@ +// The backend reaches this addon through an ESM dynamic `import()` and a named +// destructure (`const { toonEncodeToolResults } = await import(...)`). The CJS +// smoke (`require`) does not exercise that interop path, so this mirrors it: +// the named export must be exposed to ESM (via cjs-module-lexer) and callable. +import assert from "node:assert/strict"; + +const { toonEncodeToolResults } = await import("./index.cjs"); + +assert.equal(typeof toonEncodeToolResults, "function"); + +// The async binding is reachable via ESM interop and resolves positionally +// (null encoding and counts for non-JSON content). +const results = await toonEncodeToolResults( + [{ id: "esm", rawContent: "not json", unwrap: true }], + "Normalized", +); +assert.deepEqual(results, [ + { normalized: "not json", encoded: null, beforeTokens: null, encodedTokens: null }, +]); + +console.log("proxy-transform-rs esm smoke ok"); diff --git a/platform/archestra-rs/proxy-transform-rs/smoke.test.cjs b/platform/archestra-rs/proxy-transform-rs/smoke.test.cjs new file mode 100644 index 00000000000..fa23853fd29 --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/smoke.test.cjs @@ -0,0 +1,58 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const proxyTransform = require("./index.cjs"); + +// toonEncodeToolResults: a tiny batch resolves off-thread, positionally, with +// TOON output for JSON content, unwrapping applied when requested, and +// `encoded: null` for non-JSON. Heavy correctness lives in the +// proxy_transform_core cargo tests (goldens + property tests). +const inner = JSON.stringify({ data: [{ id: 1, v: "a" }, { id: 2, v: "b" }], ok: true }); +const items = [ + { id: "plain", rawContent: '{"a":1}', unwrap: false }, + { + id: "wrapped", + rawContent: JSON.stringify([{ type: "text", text: inner }]), + unwrap: true, + }, + { id: "malformed", rawContent: "not json at all", unwrap: true }, +]; + +(async () => { + const results = await proxyTransform.toonEncodeToolResults(items); + assert.equal(results.length, items.length); + + // No before-source: token counts are unset. + assert.deepEqual(results[0], { + normalized: '{"a":1}', + encoded: "a: 1", + beforeTokens: null, + encodedTokens: null, + }); + + assert.equal(results[1].normalized, inner); + assert.equal(results[1].encoded, "data[2]{id,v}:\n 1,a\n 2,b\nok: true"); + + assert.deepEqual(results[2], { + normalized: "not json at all", + encoded: null, + beforeTokens: null, + encodedTokens: null, + }); + + // With a before-source, encodable items carry cl100k counts; unencodable + // ones stay null. + const counted = await proxyTransform.toonEncodeToolResults(items, "Normalized"); + assert.ok(Number.isInteger(counted[0].beforeTokens) && counted[0].beforeTokens > 0); + assert.ok(Number.isInteger(counted[0].encodedTokens) && counted[0].encodedTokens > 0); + assert.equal(counted[2].beforeTokens, null); + assert.equal(counted[2].encodedTokens, null); + + // Empty batch resolves to an empty array. + assert.deepEqual(await proxyTransform.toonEncodeToolResults([]), []); + + console.log("proxy-transform-rs smoke ok"); +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/platform/archestra-rs/proxy-transform-rs/src/lib.rs b/platform/archestra-rs/proxy-transform-rs/src/lib.rs new file mode 100644 index 00000000000..8ff453b70fe --- /dev/null +++ b/platform/archestra-rs/proxy-transform-rs/src/lib.rs @@ -0,0 +1,84 @@ +//! Thin NAPI adapter over `proxy_transform_core`. Receives the tool-result batch +//! as owned Rust data on the JS thread, offloads the unwrap/parse/TOON-encode +//! work to the libuv threadpool, and converts a panic into a structured JS +//! error. No product logic lives here — deleting this layer must not delete the +//! core logic. The `#[napi(object)]` DTO shapes live in the core behind its +//! `napi` feature (sandbox-core pattern), so the generated `index.d.ts` mirrors +//! the core types exactly. + +use std::any::Any; + +use napi::bindgen_prelude::AsyncTask; +use napi::{Env, Task}; +use napi_derive::napi; + +use proxy_transform_core as core; + +/// Unwrap/parse/TOON-encode work for one batch of tool results, run on the libuv +/// threadpool so the JS event loop is never blocked by a large payload. The +/// items are converted to owned Rust data on the JS thread before `compute` +/// runs, so nothing here touches a JS handle. +pub struct ToonEncodeTask { + items: Vec, + count: Option, +} + +impl Task for ToonEncodeTask { + type Output = Vec; + type JsValue = Vec; + + fn compute(&mut self) -> napi::Result { + let items = std::mem::take(&mut self.items); + let count = self.count; + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + core::toon_encode_tool_results(items, count) + })) + .map_err(panic_to_napi_error) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> napi::Result { + Ok(output) + } +} + +/// Transform a batch of tool results off the JS thread: optionally unwrap the +/// `[{"type":"text","text":...}]` client wrapper, parse the JSON, and encode it +/// as TOON (spec v3). Results are positional — same length and order as +/// `items`; content that is not parseable JSON yields `encoded: null` (the +/// caller keeps the original payload). +/// +/// `beforeSource` selects the fused cl100k token counting: pass it to have the +/// off-thread pass populate `beforeTokens`/`encodedTokens` (`Normalized` for +/// most adapters, `Raw` for Gemini), or omit it to skip counting (Anthropic and +/// Bedrock tokenize with their own tokenizer). +#[napi( + js_name = "toonEncodeToolResults", + ts_return_type = "Promise>" +)] +pub fn toon_encode_tool_results( + items: Vec, + before_source: Option, +) -> AsyncTask { + AsyncTask::new(ToonEncodeTask { + items, + count: before_source, + }) +} + +fn panic_to_napi_error(payload: Box) -> napi::Error { + let body = serde_json::json!({ + "code": "ARCHESTRA_INTERNAL", + "message": format!("rust panic: {}", panic_payload_message(payload.as_ref())), + }); + napi::Error::new(napi::Status::GenericFailure, body.to_string()) +} + +fn panic_payload_message(payload: &(dyn Any + Send)) -> &str { + if let Some(s) = payload.downcast_ref::<&'static str>() { + return s; + } + if let Some(s) = payload.downcast_ref::() { + return s.as_str(); + } + "unknown panic payload" +} diff --git a/platform/backend/knip.config.ts b/platform/backend/knip.config.ts index 2e47fc61d42..074831a63cd 100644 --- a/platform/backend/knip.config.ts +++ b/platform/backend/knip.config.ts @@ -24,6 +24,7 @@ const config: KnipConfig = { "@archestra/sandbox-rs", "@archestra/app-runtime-rs", "@archestra/image-rs", + "@archestra/proxy-transform-rs", ], ignoreBinaries: [ // biome and concurrently are in root package.json diff --git a/platform/backend/package.json b/platform/backend/package.json index c00a69600c6..9bc68f8abc2 100644 --- a/platform/backend/package.json +++ b/platform/backend/package.json @@ -19,6 +19,7 @@ "dev": "tsdown --watch", "dev:debug": "DEBUG=1 tsdown --watch", "test": "vitest", + "test:native": "pnpm --filter @archestra/proxy-transform-rs build && vitest run src/routes/proxy/utils/toon-native.golden.test.ts src/routes/proxy/adapters/*-toon-compression.test.ts src/routes/proxy/routes/provider-matrix.test.ts", "lint": "biome check", "lint:fix": "biome check --write", "lint:fix:unsafe": "biome check --write --unsafe", @@ -58,6 +59,7 @@ "@anthropic-ai/tokenizer": "^0.0.4", "@archestra/app-runtime-rs": "workspace:*", "@archestra/image-rs": "workspace:*", + "@archestra/proxy-transform-rs": "workspace:*", "@archestra/sandbox-rs": "workspace:*", "@archestra/shared": "workspace:*", "@aws-crypto/sha256-js": "^5.2.0", @@ -113,7 +115,6 @@ "@smithy/eventstream-codec": "^4.2.11", "@smithy/signature-v4": "^5.3.11", "@smithy/util-utf8": "^4.2.2", - "@toon-format/toon": "^2.1.0", "@types/pdf-parse": "^1.1.5", "ai": "catalog:", "asana": "3.1.11", diff --git a/platform/backend/src/models/interaction.ts b/platform/backend/src/models/interaction.ts index 3f7cdbbcb49..440b7efa222 100644 --- a/platform/backend/src/models/interaction.ts +++ b/platform/backend/src/models/interaction.ts @@ -1087,6 +1087,7 @@ class InteractionModel { toonNotEnabledCount: sql`COUNT(*) FILTER (WHERE ${schema.interactionsTable.toonSkipReason} = 'not_enabled')`, toonNotEffectiveCount: sql`COUNT(*) FILTER (WHERE ${schema.interactionsTable.toonSkipReason} = 'not_effective')`, toonNoToolResultsCount: sql`COUNT(*) FILTER (WHERE ${schema.interactionsTable.toonSkipReason} = 'no_tool_results')`, + toonAddonUnavailableCount: sql`COUNT(*) FILTER (WHERE ${schema.interactionsTable.toonSkipReason} = 'addon_unavailable')`, firstRequestTime: min(schema.interactionsTable.createdAt), lastRequestTime: max(schema.interactionsTable.createdAt), models: sql`STRING_AGG(DISTINCT ${schema.interactionsTable.model}, ',')`, @@ -1188,6 +1189,7 @@ class InteractionModel { notEnabled: Number(s.toonNotEnabledCount) || 0, notEffective: Number(s.toonNotEffectiveCount) || 0, noToolResults: Number(s.toonNoToolResultsCount) || 0, + addonUnavailable: Number(s.toonAddonUnavailableCount) || 0, }, firstRequestTime: s.firstRequestTime ?? new Date(), lastRequestTime: s.lastRequestTime ?? new Date(), diff --git a/platform/backend/src/observability/metrics/llm.ts b/platform/backend/src/observability/metrics/llm.ts index 5e10b9ccb93..456c1730e48 100644 --- a/platform/backend/src/observability/metrics/llm.ts +++ b/platform/backend/src/observability/metrics/llm.ts @@ -842,6 +842,27 @@ export function reportKbLlmCall(params: { } } +// Lazily registered instead of inside initializeMetrics(): it has no dynamic +// agent labels, must never be reset when label keys change, and load failures +// can occur before metrics initialization (the startup probe). +let llmToonAddonLoadFailures: client.Counter | undefined; + +/** + * Reports a failure to load or call the native proxy-transform addon. + * TOON tool-result compression fails open (skipped) when this fires, with the + * `addon_unavailable` skip reason persisted on the interaction. + */ +export function reportToonAddonUnavailable( + context: "startup" | "request", +): void { + llmToonAddonLoadFailures ??= new client.Counter({ + name: "llm_toon_addon_load_failures_total", + help: "Failures to load or call the native proxy-transform addon (TOON compression fails open and is skipped)", + labelNames: ["context"], + }); + llmToonAddonLoadFailures.inc({ context }); +} + function extractHeaderNames( headers: HeadersInit | undefined, ): Record { diff --git a/platform/backend/src/routes/proxy/__bench__/bench-concurrency.ts b/platform/backend/src/routes/proxy/__bench__/bench-concurrency.ts new file mode 100644 index 00000000000..86469d005f9 --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-concurrency.ts @@ -0,0 +1,89 @@ +/** + * Benchmark (c): concurrency guardrail scenario. + * + * Runs 8 concurrent async batches of the TOON kernel (mixed 1KB-5MB items, + * ~8.8MB per batch, ~70MB total) with an event-loop yield between items, + * and reports p50/p99/max event-loop delay plus peak RSS, through the native + * backend (see toon-backend.ts). + * + * Run from platform/backend: + * pnpm exec tsx src/routes/proxy/__bench__/bench-concurrency.ts + */ +import { monitorEventLoopDelay, performance } from "node:perf_hooks"; +import { setImmediate as yieldEventLoop } from "node:timers/promises"; +import { fmt } from "./bench-util"; +import { batchBytes, buildBatch, type CorpusSpec } from "./corpus"; +import { encodeBatchNative, type ToonKernelItem } from "./toon-backend"; + +const CONCURRENCY = 8; + +const WORKER_PARTS: CorpusSpec[] = [ + { name: "w-5MB", payloadBytes: 5 << 20, count: 1 }, + { name: "w-1MB", payloadBytes: 1 << 20, count: 2 }, + { name: "w-100KB", payloadBytes: 100 << 10, count: 10 }, + { name: "w-10KB", payloadBytes: 10 << 10, count: 20 }, + { name: "w-1KB", payloadBytes: 1 << 10, count: 50 }, +]; + +function buildWorkerBatch(seed: number): ToonKernelItem[] { + return WORKER_PARTS.flatMap((part, i) => buildBatch(part, seed + i * 31)); +} + +// Prevents dead-code elimination of the encode results. +let sink = 0; +let peakRss = 0; + +function sampleRss(): void { + const rss = process.memoryUsage().rss; + if (rss > peakRss) { + peakRss = rss; + } +} + +async function worker(items: ToonKernelItem[]): Promise { + for (const item of items) { + const [result] = await encodeBatchNative([item]); + sink += result.encoded === null ? 0 : result.encoded.length; + sampleRss(); + await yieldEventLoop(); + } +} + +async function main(): Promise { + const batches: ToonKernelItem[][] = []; + for (let i = 0; i < CONCURRENCY; i++) { + batches.push(buildWorkerBatch(1000 + i)); + } + const totalMB = + batches.reduce((sum, b) => sum + batchBytes(b), 0) / (1 << 20); + const baselineRssMB = process.memoryUsage().rss / (1 << 20); + + const histogram = monitorEventLoopDelay({ resolution: 10 }); + const rssTimer = setInterval(sampleRss, 25); + histogram.enable(); + const start = performance.now(); + await Promise.all(batches.map((b) => worker(b))); + const wallMs = performance.now() - start; + histogram.disable(); + clearInterval(rssTimer); + sampleRss(); + + const toMs = (ns: number) => ns / 1e6; + console.info( + "bench-concurrency: 8 concurrent TOON kernel batches (native backend)", + ); + console.info( + [ + `total=${fmt(totalMB, 1)}MB`, + `wall=${fmt(wallMs)}ms`, + `elDelay p50=${fmt(toMs(histogram.percentile(50)))}ms`, + `p99=${fmt(toMs(histogram.percentile(99)))}ms`, + `max=${fmt(toMs(histogram.max))}ms`, + `rss baseline=${fmt(baselineRssMB, 1)}MB`, + `peak=${fmt(peakRss / (1 << 20), 1)}MB`, + ].join(" "), + ); + console.info(`(sink=${sink})`); +} + +main(); diff --git a/platform/backend/src/routes/proxy/__bench__/bench-env.ts b/platform/backend/src/routes/proxy/__bench__/bench-env.ts new file mode 100644 index 00000000000..9e2e4daf83c --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-env.ts @@ -0,0 +1,10 @@ +/** + * Side-effect module for the T0 benchmark harness. Import it FIRST in any + * bench script that pulls in the backend module graph: `@/config` requires a + * database URL at import time. The URL below is never dialed by the + * benchmarks — it only satisfies config validation. + */ +process.env.ARCHESTRA_DATABASE_URL ??= + "postgres://bench:bench@127.0.0.1:5432/bench"; +// Quiet per-message info logs from the adapter path during runs. +process.env.ARCHESTRA_LOGGING_LEVEL ??= "warn"; diff --git a/platform/backend/src/routes/proxy/__bench__/bench-stream-replay.ts b/platform/backend/src/routes/proxy/__bench__/bench-stream-replay.ts new file mode 100644 index 00000000000..5093f66e44a --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-stream-replay.ts @@ -0,0 +1,120 @@ +/** + * Benchmark (b): streaming tool-call replay quadratic path. + * + * Feeds OpenAIStreamAdapter a synthetic stream of tool-call argument + * fragments and, after every fragment chunk, calls getRawToolCallEvents() + * plus the handler's written-index dedup loop — mimicking the per-chunk + * non-blocking-policy path at llm-proxy-handler.ts:1214-1221. The + * re-serialization of the full event history on every call is the O(k^2) + * term this baseline pins down. + * + * Run from platform/backend: + * pnpm exec tsx src/routes/proxy/__bench__/bench-stream-replay.ts + */ +import "./bench-env"; +import { performance } from "node:perf_hooks"; +import type { OpenAi } from "@/types"; +import { OpenAIStreamAdapter } from "../adapters/openai"; +import { fmt, summarize } from "./bench-util"; + +type Chunk = OpenAi.Types.ChatCompletionChunk; + +const FRAGMENT_COUNTS = [100, 500, 1000, 2000]; +const REPEATS = 5; +const FRAGMENT = '{"query":"synthetic fragment payload #'; + +function makeChunk(fragmentIndex: number): Chunk { + const first = fragmentIndex === 0; + return { + id: "chatcmpl-bench", + object: "chat.completion.chunk", + created: 1_700_000_000, + model: "gpt-bench", + choices: [ + { + index: 0, + delta: { + tool_calls: [ + first + ? { + index: 0, + id: "call_bench_0", + type: "function", + function: { name: "search_documents", arguments: "" }, + } + : { + index: 0, + function: { arguments: `${FRAGMENT}${fragmentIndex}"}` }, + }, + ], + }, + finish_reason: null, + logprobs: null, + }, + ], + }; +} + +// Prevents dead-code elimination of the replayed SSE strings. +let sink = 0; + +function runScenario(fragmentCount: number): number { + const adapter = new OpenAIStreamAdapter("openai"); + const streamedEventIndices = new Set(); + const chunks: Chunk[] = []; + for (let i = 0; i < fragmentCount; i++) { + chunks.push(makeChunk(i)); + } + + const start = performance.now(); + for (const chunk of chunks) { + const result = adapter.processChunk(chunk); + if (result.isToolCallChunk) { + // Handler's per-chunk replay + dedup (llm-proxy-handler.ts:1214-1221). + const allEvents = adapter.getRawToolCallEvents(); + for (let i = 0; i < allEvents.length; i++) { + if (!streamedEventIndices.has(i)) { + sink += allEvents[i].length; + streamedEventIndices.add(i); + } + } + } + } + return performance.now() - start; +} + +console.info( + "bench-stream-replay: per-chunk getRawToolCallEvents replay (baseline)", +); +const perCount = new Map(); +for (const fragmentCount of FRAGMENT_COUNTS) { + runScenario(fragmentCount); // warmup + const samples: number[] = []; + for (let r = 0; r < REPEATS; r++) { + samples.push(runScenario(fragmentCount)); + } + const s = summarize(samples); + perCount.set(fragmentCount, s.meanMs); + console.info( + [ + `fragments=${String(fragmentCount).padStart(5)}`, + `mean=${fmt(s.meanMs).padStart(9)}ms`, + `stdev=${fmt(s.stdevMs).padStart(7)}ms`, + `min=${fmt(s.minMs).padStart(9)}ms`, + `perChunk=${fmt(s.meanMs / fragmentCount, 4).padStart(8)}ms`, + ].join(" "), + ); +} + +const base = perCount.get(FRAGMENT_COUNTS[0]); +if (base !== undefined && base > 0) { + for (const fragmentCount of FRAGMENT_COUNTS.slice(1)) { + const ratio = (perCount.get(fragmentCount) ?? 0) / base; + const kRatio = fragmentCount / FRAGMENT_COUNTS[0]; + console.info( + `scaling ${FRAGMENT_COUNTS[0]} -> ${fragmentCount}: time x${fmt(ratio, 1)} ` + + `(linear would be x${fmt(kRatio, 0)}, quadratic x${fmt(kRatio ** 2, 0)})`, + ); + } +} +console.info(`(sink=${sink})`); diff --git a/platform/backend/src/routes/proxy/__bench__/bench-token-count.ts b/platform/backend/src/routes/proxy/__bench__/bench-token-count.ts new file mode 100644 index 00000000000..c96c37edab2 --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-token-count.ts @@ -0,0 +1,267 @@ +/** + * Profiling bench: what does the JS token counting on the TOON keep/reject + * decision path actually cost, and would moving it into the off-thread native + * kernel be justified? + * + * The current path per request is: 1 native encode (off-thread, libuv) then + * `2 x N` synchronous `getTokenizer(provider).countTokens([...])` calls on the + * event loop (before = normalized, after = encoded), each a WASM cl100k encode + * unless the per-message memo (base.ts) hits. + * + * This measures three regimes that decide the question: + * 1. sequential per-candidate cost, COLD (unique content, cache miss — Rust's + * win case) vs WARM (repeated history, cache hit — Rust's regression risk); + * 2. event-loop delay under 8-way concurrency with counting ON vs OFF (what + * off-loading the synchronous counting to libuv would recover); + * 3. transient JS heap allocation from the token-id arrays. + * + * Run from platform/backend (add --expose-gc for the retained/transient split): + * pnpm exec tsx src/routes/proxy/__bench__/bench-token-count.ts + * node --expose-gc --import tsx src/routes/proxy/__bench__/bench-token-count.ts + */ +import "./bench-env"; +import { monitorEventLoopDelay, performance } from "node:perf_hooks"; +import { setImmediate as yieldEventLoop } from "node:timers/promises"; +import { getTokenizer } from "@/tokenizers"; +import { toonEncodeToolResults } from "../utils/toon-native"; +import { fmt, summarize } from "./bench-util"; +import { + batchBytes, + buildBatch, + CORPUS_SPECS, + type CorpusSpec, +} from "./corpus"; +import { encodeBatchNative, type ToonKernelItem } from "./toon-backend"; + +// All five tiktoken-family TOON adapters count with cl100k under role "user" +// (see base.ts getEncodableText). openai stands in for the whole family. +const PROVIDER = "openai" as const; +const TIME_BUDGET_MS = 3_000; +const MIN_ITERATIONS = 5; +const MAX_ITERATIONS = 400; +const CONCURRENCY = 8; + +let sink = 0; + +// A candidate as the decision loop sees it: the two strings it tokenizes. +interface Countable { + before: string; + after: string; +} + +/** Run the batch through the real native encoder, keep only encodable items. */ +async function encodeCountables(items: ToonKernelItem[]): Promise { + const results = await encodeBatchNative(items); + const countables: Countable[] = []; + results.forEach((r, i) => { + // Skip non-JSON / budget-rejected items — the adapters skip counting them. + if (r.encoded !== null) { + countables.push({ before: items[i].rawContent, after: r.encoded }); + } + }); + return countables; +} + +/** The exact per-candidate work the adapters do: two role-"user" counts. */ +function countOnce(items: Countable[], nonce: string): void { + const tokenizer = getTokenizer(PROVIDER); + for (const { before, after } of items) { + // A non-empty nonce forces a fresh cache key (cold path); "" replays the + // same strings so the second+ pass hits the memo (warm path). + const b = tokenizer.countTokens([ + { role: "user", content: nonce + before }, + ]); + const a = tokenizer.countTokens([{ role: "user", content: nonce + after }]); + sink += b + a; + } +} + +function timedIterations(run: (iter: number) => void): { + meanMs: number; + minMs: number; + iterations: number; +} { + const samples: number[] = []; + const budgetStart = performance.now(); + while ( + samples.length < MAX_ITERATIONS && + (samples.length < MIN_ITERATIONS || + performance.now() - budgetStart < TIME_BUDGET_MS) + ) { + const start = performance.now(); + run(samples.length); + samples.push(performance.now() - start); + } + const s = summarize(samples); + return { meanMs: s.meanMs, minMs: s.minMs, iterations: s.iterations }; +} + +async function section1Sequential(): Promise { + console.info( + "\n[1] Sequential JS token counting cost per candidate (cl100k, 2 counts each)", + ); + console.info( + " warm = repeated content (memo hit); cold = unique content (memo miss, real WASM encode)", + ); + for (const spec of CORPUS_SPECS) { + if (spec.payloadBytes > 1 << 20) { + continue; // 1KB..1MB spans realistic tool-result sizes; skip 5MB here. + } + const items = await encodeCountables(buildBatch(spec, 42)); + if (items.length === 0) { + continue; + } + // Warm: same strings every pass — after the first, all memo hits. + const warm = timedIterations(() => countOnce(items, "")); + // Cold: a unique per-iteration prefix defeats the memo every pass. + const cold = timedIterations((iter) => countOnce(items, `c${iter}_`)); + + const perCandWarmUs = (warm.meanMs / items.length) * 1000; + const perCandColdUs = (cold.meanMs / items.length) * 1000; + console.info( + [ + spec.name.padEnd(6), + `cand=${String(items.length).padStart(4)}`, + `cold=${fmt(perCandColdUs, 1).padStart(8)}us/cand`, + `warm=${fmt(perCandWarmUs, 2).padStart(8)}us/cand`, + `cold/warm=${fmt(perCandColdUs / Math.max(perCandWarmUs, 1e-6), 0).padStart(5)}x`, + `coldBatch=${fmt(cold.minMs).padStart(8)}ms`, + ].join(" "), + ); + } +} + +async function section2EventLoop(): Promise { + console.info("\n[2] Event-loop delay under 8 concurrent batches:"); + console.info( + " none = encode only; js = encode + synchronous WASM count (old); native = encode + fused count (new, off-thread)", + ); + const batches: ToonKernelItem[][] = []; + for (let i = 0; i < CONCURRENCY; i++) { + // Mixed 1KB..100KB, ~realistic tool-result batches. + batches.push( + [ + { name: "100KB", payloadBytes: 100 << 10, count: 8 }, + { name: "10KB", payloadBytes: 10 << 10, count: 24 }, + { name: "1KB", payloadBytes: 1 << 10, count: 48 }, + ].flatMap((p: CorpusSpec, j) => buildBatch(p, 2000 + i * 17 + j * 5)), + ); + } + const totalMB = batches.reduce((s, b) => s + batchBytes(b), 0) / (1 << 20); + + // Each worker processes items one at a time with an event-loop yield between + // them, mirroring bench-concurrency.ts. This isolates event-loop delay (the + // metric that matters): a production request batches its results into one + // native call, so the absolute wall/RSS figures here are directional, but the + // p50/p99/max event-loop delay per mode is faithful — synchronous JS counting + // stalls the loop, the off-thread native call does not. + for (const mode of ["none", "js", "native"] as const) { + let peakRss = 0; + const sampleRss = () => { + const rss = process.memoryUsage().rss; + if (rss > peakRss) { + peakRss = rss; + } + }; + const tokenizer = getTokenizer(PROVIDER); + const worker = async (items: ToonKernelItem[]) => { + for (const item of items) { + if (mode === "native") { + // New path: encode + count in one off-thread native call. + const results = await toonEncodeToolResults( + [{ id: "b", rawContent: item.rawContent, unwrap: item.unwrap }], + "normalized", + ); + const r = results?.[0]; + if (r) { + sink += (r.beforeTokens ?? 0) + (r.encodedTokens ?? 0); + } + } else { + const [r] = await encodeBatchNative([item]); + if (mode === "js" && r.encoded !== null) { + // Old path: synchronous WASM counting on the event loop, unique per + // item so the memo never hides the cost (worst case). + sink += tokenizer.countTokens([ + { + role: "user", + content: `${item.rawContent.length}:${item.rawContent}`, + }, + ]); + sink += tokenizer.countTokens([ + { role: "user", content: r.encoded }, + ]); + } + } + sampleRss(); + await yieldEventLoop(); + } + }; + const histogram = monitorEventLoopDelay({ resolution: 10 }); + const rssTimer = setInterval(sampleRss, 25); + histogram.enable(); + const start = performance.now(); + await Promise.all(batches.map((b) => worker(b))); + const wallMs = performance.now() - start; + histogram.disable(); + clearInterval(rssTimer); + const toMs = (ns: number) => ns / 1e6; + console.info( + [ + `mode=${mode.padEnd(6)}`, + `total=${fmt(totalMB, 1)}MB`, + `wall=${fmt(wallMs).padStart(9)}ms`, + `elDelay p50=${fmt(toMs(histogram.percentile(50))).padStart(7)}ms`, + `p99=${fmt(toMs(histogram.percentile(99))).padStart(8)}ms`, + `max=${fmt(toMs(histogram.max)).padStart(9)}ms`, + `peakRss=${fmt(peakRss / (1 << 20), 1)}MB`, + ].join(" "), + ); + } +} + +async function section3Memory(): Promise { + console.info( + "\n[3] Transient JS heap from token-id arrays (cold counting of a 100KB x 64 batch)", + ); + const items = await encodeCountables( + buildBatch({ name: "100KB", payloadBytes: 100 << 10, count: 64 }, 7), + ); + const gc = (globalThis as { gc?: () => void }).gc; + if (gc) { + gc(); + } + const before = process.memoryUsage(); + const PASSES = 50; + for (let p = 0; p < PASSES; p++) { + countOnce(items, `m${p}_`); // cold every pass + } + const afterNoGc = process.memoryUsage(); + if (gc) { + gc(); + } + const afterGc = process.memoryUsage(); + const mb = (n: number) => fmt(n / (1 << 20), 1); + console.info( + ` heapUsed before=${mb(before.heapUsed)}MB afterLoop=${mb(afterNoGc.heapUsed)}MB afterGC=${mb(afterGc.heapUsed)}MB`, + ); + console.info( + ` external before=${mb(before.external)}MB afterLoop=${mb(afterNoGc.external)}MB afterGC=${mb(afterGc.external)}MB`, + ); + console.info( + gc + ? " (afterGC ~= before means the counting churn is fully transient, not retained)" + : " (re-run with --expose-gc to separate transient churn from retained heap)", + ); +} + +async function main(): Promise { + console.info( + "bench-token-count: current JS-side cl100k counting on the TOON decision path", + ); + await section1Sequential(); + await section2EventLoop(); + await section3Memory(); + console.info(`\n(sink=${sink})`); +} + +main(); diff --git a/platform/backend/src/routes/proxy/__bench__/bench-toon-kernel.ts b/platform/backend/src/routes/proxy/__bench__/bench-toon-kernel.ts new file mode 100644 index 00000000000..befec271e8e --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-toon-kernel.ts @@ -0,0 +1,77 @@ +/** + * Benchmark (a): TOON kernel (unwrap -> JSON.parse -> TOON encode) over + * deterministic synthetic corpora, through the native backend + * (see toon-backend.ts). + * + * Run from platform/backend: + * pnpm exec tsx src/routes/proxy/__bench__/bench-toon-kernel.ts + */ +import { performance } from "node:perf_hooks"; +import { fmt, summarize } from "./bench-util"; +import { + batchBytes, + buildBatch, + buildJumboBatch, + CORPUS_SPECS, +} from "./corpus"; +import { encodeBatchNative, type ToonKernelItem } from "./toon-backend"; + +const TIME_BUDGET_MS = 4_000; +const MIN_ITERATIONS = 5; +const MAX_ITERATIONS = 200; + +// Prevents dead-code elimination of the encode results. +let sink = 0; + +async function runBatch(items: ToonKernelItem[]): Promise { + const start = performance.now(); + const results = await encodeBatchNative(items); + const elapsed = performance.now() - start; + for (const r of results) { + sink += r.encoded === null ? r.normalized.length : r.encoded.length; + } + return elapsed; +} + +async function benchCorpus( + name: string, + items: ToonKernelItem[], +): Promise { + const totalMB = batchBytes(items) / (1 << 20); + await runBatch(items); // warmup + const samples: number[] = []; + const budgetStart = performance.now(); + while ( + samples.length < MAX_ITERATIONS && + (samples.length < MIN_ITERATIONS || + performance.now() - budgetStart < TIME_BUDGET_MS) + ) { + samples.push(await runBatch(items)); + } + const s = summarize(samples); + const mbPerSec = totalMB / (s.meanMs / 1000); + console.info( + [ + name.padEnd(6), + `items=${String(items.length).padStart(4)}`, + `total=${fmt(totalMB, 1).padStart(6)}MB`, + `iters=${String(s.iterations).padStart(3)}`, + `mean=${fmt(s.meanMs).padStart(9)}ms/batch`, + `stdev=${fmt(s.stdevMs).padStart(7)}ms`, + `min=${fmt(s.minMs).padStart(9)}ms`, + `throughput=${fmt(mbPerSec, 1).padStart(7)}MB/s`, + `perItem=${fmt(s.meanMs / items.length, 3).padStart(9)}ms`, + ].join(" "), + ); +} + +async function main(): Promise { + console.info("bench-toon-kernel: native backend"); + for (const spec of CORPUS_SPECS) { + await benchCorpus(spec.name, buildBatch(spec, 42)); + } + await benchCorpus("70MB", buildJumboBatch(4242)); + console.info(`(sink=${sink})`); +} + +main(); diff --git a/platform/backend/src/routes/proxy/__bench__/bench-util.ts b/platform/backend/src/routes/proxy/__bench__/bench-util.ts new file mode 100644 index 00000000000..7db8886e425 --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/bench-util.ts @@ -0,0 +1,25 @@ +export interface SampleStats { + iterations: number; + meanMs: number; + stdevMs: number; + minMs: number; + maxMs: number; +} + +export function summarize(samplesMs: number[]): SampleStats { + const n = samplesMs.length; + const meanMs = samplesMs.reduce((a, b) => a + b, 0) / n; + const variance = + n > 1 ? samplesMs.reduce((a, b) => a + (b - meanMs) ** 2, 0) / (n - 1) : 0; + return { + iterations: n, + meanMs, + stdevMs: Math.sqrt(variance), + minMs: Math.min(...samplesMs), + maxMs: Math.max(...samplesMs), + }; +} + +export function fmt(value: number, digits = 2): string { + return value.toFixed(digits); +} diff --git a/platform/backend/src/routes/proxy/__bench__/corpus.ts b/platform/backend/src/routes/proxy/__bench__/corpus.ts new file mode 100644 index 00000000000..5a77d3a2ebb --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/corpus.ts @@ -0,0 +1,172 @@ +import type { ToonKernelItem } from "./toon-backend"; + +/** + * Deterministic synthetic corpora for the TOON kernel benchmarks (T0). + * + * Every batch is fully reproducible from its seed. Item mix per 10 items: + * 6 uniform JSON arrays of objects, 2 non-array JSON objects, 2 non-JSON + * prose strings; 2 of the JSON items are wrapped in the n8n/Vercel-style + * `[{"type":"text","text":...}]` wrapper (exercising the unwrap path), + * and roughly 1 in 7 unwrapped items uses `unwrap: false` (Bedrock-style). + */ + +export interface CorpusSpec { + name: string; + payloadBytes: number; + count: number; +} + +export const CORPUS_SPECS: CorpusSpec[] = [ + { name: "1KB", payloadBytes: 1 << 10, count: 256 }, + { name: "10KB", payloadBytes: 10 << 10, count: 128 }, + { name: "100KB", payloadBytes: 100 << 10, count: 64 }, + { name: "1MB", payloadBytes: 1 << 20, count: 16 }, + { name: "5MB", payloadBytes: 5 << 20, count: 8 }, +]; + +export function buildBatch(spec: CorpusSpec, seed: number): ToonKernelItem[] { + const rng = mulberry32(seed); + const items: ToonKernelItem[] = []; + for (let i = 0; i < spec.count; i++) { + const r = i % 10; + const kind: PayloadKind = + r === 2 || r === 7 ? "object" : r === 4 || r === 9 ? "nonjson" : "array"; + const wrapped = kind !== "nonjson" && (r === 6 || r === 7); + let payload = buildPayload(kind, spec.payloadBytes, rng); + if (wrapped) { + payload = JSON.stringify([{ type: "text", text: payload }]); + } + // Wrapped payloads must be unwrapped to reach the inner JSON; a slice of + // plain payloads mimics the Bedrock branches, which never unwrap. + const unwrap = wrapped ? true : i % 7 !== 3; + items.push({ rawContent: payload, unwrap }); + } + return items; +} + +/** Mixed-size ~70MB batch approximating the proxy body limit. */ +export function buildJumboBatch(seed: number): ToonKernelItem[] { + const parts: CorpusSpec[] = [ + { name: "jumbo-5MB", payloadBytes: 5 << 20, count: 8 }, // ~40MB + { name: "jumbo-1MB", payloadBytes: 1 << 20, count: 16 }, // ~16MB + { name: "jumbo-100KB", payloadBytes: 100 << 10, count: 96 }, // ~9.4MB + { name: "jumbo-10KB", payloadBytes: 10 << 10, count: 300 }, // ~2.9MB + { name: "jumbo-1KB", payloadBytes: 1 << 10, count: 1024 }, // ~1MB + ]; + return parts.flatMap((part, i) => buildBatch(part, seed + i * 101)); +} + +export function batchBytes(items: ToonKernelItem[]): number { + return items.reduce((sum, item) => sum + item.rawContent.length, 0); +} + +// ============================================================================= +// INTERNALS +// ============================================================================= + +type PayloadKind = "array" | "object" | "nonjson"; + +function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const WORDS = [ + "alpha", + "bravo", + "charlie", + "delta", + "echo", + "foxtrot", + "golf", + "hotel", + "india", + "juliet", + "kilo", + "lima", + "mike", + "november", + "oscar", + "papa", + "quebec", + "romeo", + "sierra", + "tango", + "uniform", + "victor", + "whiskey", + "zulu", +]; + +const STATUSES = ["active", "pending", "archived", "failed"]; + +function pick(rng: () => number, pool: T[]): T { + return pool[Math.floor(rng() * pool.length)]; +} + +function makeRow(rng: () => number, id: number): Record { + return { + id, + sku: `SKU-${Math.floor(rng() * 1_000_000)}`, + name: `${pick(rng, WORDS)} ${pick(rng, WORDS)}`, + status: pick(rng, STATUSES), + score: Math.round(rng() * 10_000) / 100, + quantity: Math.floor(rng() * 500), + active: rng() > 0.5, + updatedAt: `2026-0${1 + Math.floor(rng() * 6)}-${String(1 + Math.floor(rng() * 28)).padStart(2, "0")}T12:00:00Z`, + }; +} + +function buildPayload( + kind: PayloadKind, + targetBytes: number, + rng: () => number, +): string { + switch (kind) { + case "array": { + const rows: Record[] = []; + let size = 2; // brackets + let id = 0; + while (size < targetBytes) { + const row = makeRow(rng, id++); + size += JSON.stringify(row).length + 1; + rows.push(row); + } + return JSON.stringify(rows); + } + case "object": { + const entries: Record = {}; + let size = 64; + let id = 0; + while (size < targetBytes) { + const key = `entry_${id}`; + const value = { + ...makeRow(rng, id), + nested: { tags: [pick(rng, WORDS), pick(rng, WORDS)], depth: 2 }, + }; + size += JSON.stringify(value).length + key.length + 4; + entries[key] = value; + id++; + } + return JSON.stringify({ + meta: { source: "bench", version: 3, total: id }, + entries, + }); + } + case "nonjson": { + const parts: string[] = [`Tool run ${Math.floor(rng() * 1000)} output:`]; + let size = parts[0].length; + while (size < targetBytes) { + const word = pick(rng, WORDS); + parts.push(word); + size += word.length + 1; + } + return parts.join(" "); + } + } +} diff --git a/platform/backend/src/routes/proxy/__bench__/toon-backend.ts b/platform/backend/src/routes/proxy/__bench__/toon-backend.ts new file mode 100644 index 00000000000..d25bd4e099f --- /dev/null +++ b/platform/backend/src/routes/proxy/__bench__/toon-backend.ts @@ -0,0 +1,39 @@ +/** + * TOON kernel backend for the benchmark harness (T0/T8): the real production + * helper (utils/toon-native.ts) over the Rust addon, so JS→Rust string + * copies, async scheduling, and result conversion are all inside the + * measurement. Boundary: batch of { rawContent, unwrap } → results. + * + * The TS reference backend this was originally compared against (npm + * @toon-format/toon) was removed once all adapters cut over to the native + * kernel — baseline numbers are recorded in the PR; git history keeps the + * code. + */ +import "./bench-env"; +import { toonEncodeToolResults } from "../utils/toon-native"; + +export interface ToonKernelItem { + rawContent: string; + unwrap: boolean; +} + +export interface ToonKernelResult { + normalized: string; + encoded: string | null; +} + +export async function encodeBatchNative( + items: ToonKernelItem[], +): Promise { + const results = await toonEncodeToolResults( + items.map(({ rawContent, unwrap }, i) => ({ + id: `bench_${i}`, + rawContent, + unwrap, + })), + ); + if (results === null) { + throw new Error("native TOON backend unavailable (addon failed to load)"); + } + return results; +} diff --git a/platform/backend/src/routes/proxy/adapters/anthropic-openai.ts b/platform/backend/src/routes/proxy/adapters/anthropic-openai.ts index 1877e08acfb..9a251886b05 100644 --- a/platform/backend/src/routes/proxy/adapters/anthropic-openai.ts +++ b/platform/backend/src/routes/proxy/adapters/anthropic-openai.ts @@ -159,7 +159,12 @@ class AnthropicOpenaiStreamAdapter } getRawToolCallEvents(): string[] { - return this.pendingToolCallEvents.splice(0); + // Full append-only history, per the LLMStreamAdapter contract: the handler + // dedups replayed events by array index, so draining the buffer here + // (as an earlier splice(0) implementation did) re-based indices at 0 and + // made the handler drop every tool-call argument delta after the first + // event — and flush nothing at all for buffered-then-approved tool calls. + return this.pendingToolCallEvents; } formatCompleteTextSSE(text: string): string[] { diff --git a/platform/backend/src/routes/proxy/adapters/anthropic-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/anthropic-toon-compression.test.ts new file mode 100644 index 00000000000..e8ba0e047f1 --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/anthropic-toon-compression.test.ts @@ -0,0 +1,351 @@ +// Pins the Anthropic adapter's TOON compression cutover to the native addon: +// full transformed-request exact equality (TOON content from the committed v3 +// golden corpus, everything else byte-equal). Anthropic-specific semantics +// pinned here: candidates come from BOTH tool_result content shapes (string +// content and every text sub-block of array content — several blocks can +// share one tool_use_id), each text block is counted individually, rejected +// payloads count their original tokens in both totals, and hadToolResults +// reflects every non-error tool_result block (even unparseable ones). +// Requires the built addon: mandatory in CI; locally it skips visibly — run +// `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Anthropic } from "@/types"; +import { anthropicAdapterFactory } from "./anthropic"; + +type AnthropicRequest = Anthropic.Types.MessagesRequest; + +// Corpus picks: a uniform array (compression wins), a wrapped +// [{type:"text",...}] payload (unwrapped, compression wins), malformed JSON +// (kept as-is), and a near-boundary object whose TOON encoding does not save +// tokens under the Anthropic tokenizer (rejected). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const MALFORMED = corpusEntry("malformed-prose"); +const NEAR_BOUNDARY = corpusEntry("boundary-obj-1"); + +const countTokens = makeCountTokens("anthropic"); +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + +describeNative("Anthropic adapter TOON compression (native addon)", () => { + test("transforms the full provider request exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): AnthropicRequest => ({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + temperature: 0.25, + messages: [ + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu_list", + name: "list_files", + input: { directory: "." }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_uniform", + content: UNIFORM.rawContent, + }, + { + type: "tool_result", + tool_use_id: "toolu_wrapped", + content: WRAPPED.rawContent, + }, + { + type: "tool_result", + tool_use_id: "toolu_malformed", + content: MALFORMED.rawContent, + }, + { + type: "tool_result", + tool_use_id: "toolu_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }, + ], + }); + + const adapter = anthropicAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + const expectedRequest = makeRequest(); + // Compression wins for the uniform array and the wrapped payload; the + // malformed and near-boundary results keep their original content. + expectedRequest.messages[2].content = [ + { + type: "tool_result", + tool_use_id: "toolu_uniform", + content: UNIFORM.expected.encoded as string, + }, + { + type: "tool_result", + tool_use_id: "toolu_wrapped", + content: WRAPPED.expected.encoded as string, + }, + { + type: "tool_result", + tool_use_id: "toolu_malformed", + content: MALFORMED.rawContent, + }, + { + type: "tool_result", + tool_use_id: "toolu_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ]; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + // Rejected payloads count their original tokens in BOTH totals; malformed + // content is not counted at all. + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized) + + boundaryTokens; + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string) + + boundaryTokens; + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("compresses every text block of a multi-block tool_result sharing one tool_use_id", async () => { + await upsertPricing(); + + // One tool_result carries three text blocks under a single tool_use_id: + // positional (locator-based) application must compress the first and + // second independently while keeping the malformed third — matching by + // id would misapply results. + const makeRequest = (): AnthropicRequest => ({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + messages: [ + { role: "user", content: "Fetch the data." }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_multi", + content: [ + { type: "text", text: UNIFORM.rawContent }, + { type: "text", text: WRAPPED.rawContent }, + { type: "text", text: MALFORMED.rawContent }, + ], + }, + ], + }, + ], + }); + + const adapter = anthropicAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + const expectedRequest = makeRequest(); + expectedRequest.messages[1].content = [ + { + type: "tool_result", + tool_use_id: "toolu_multi", + content: [ + { type: "text", text: UNIFORM.expected.encoded as string }, + { type: "text", text: WRAPPED.expected.encoded as string }, + { type: "text", text: MALFORMED.rawContent }, + ], + }, + ]; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + // Each text block is counted individually. + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized); + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right blocks when non-candidate blocks are interleaved", async () => { + await upsertPricing(); + + // Non-candidates between candidates: a plain text block, an is_error + // tool_result (whose content WOULD compress if wrongly collected), and a + // malformed text block inside an array tool_result. The native result + // index diverges from every structural index — off-by-one positional + // application would compress the wrong block and fail the equality below. + const makeRequest = (): AnthropicRequest => ({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + messages: [ + { role: "user", content: "Inspect the workspace." }, + { + role: "user", + content: [ + { type: "text", text: "status update" }, + { + type: "tool_result", + tool_use_id: "toolu_error", + is_error: true, + content: UNIFORM.rawContent, + }, + { + type: "tool_result", + tool_use_id: "toolu_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }, + { role: "assistant", content: "Checking the results." }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_mixed", + content: [ + { type: "text", text: MALFORMED.rawContent }, + { type: "text", text: UNIFORM.rawContent }, + ], + }, + ], + }, + ], + }); + + const adapter = anthropicAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + // Only the uniform text block compresses; the error result, the + // near-boundary result, and the malformed block are untouched. + const expectedRequest = makeRequest(); + expectedRequest.messages[3].content = [ + { + type: "tool_result", + tool_use_id: "toolu_mixed", + content: [ + { type: "text", text: MALFORMED.rawContent }, + { type: "text", text: UNIFORM.expected.encoded as string }, + ], + }, + ]; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + boundaryTokens + countTokens(UNIFORM.expected.normalized); + const tokensAfter = + boundaryTokens + countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // The case the combined request cannot isolate: unlike the OpenAI family, + // hadToolResults is true even when the only non-error tool_result block is + // malformed — Anthropic counts every block it sees. + test("a lone malformed result: zeroed totals but hadToolResults=true, message untouched", async () => { + await upsertPricing(); + + const makeRequest = (): AnthropicRequest => ({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + messages: [ + { role: "user", content: "run the tool" }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_1", + content: MALFORMED.rawContent, + }, + ], + }, + ], + }); + + const adapter = anthropicAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + expect(adapter.toProviderRequest()).toStrictEqual(makeRequest()); + }); + + test("a lone rejected result: original counted in both totals, wasEffective=false", async () => { + await upsertPricing(); + + const makeRequest = (): AnthropicRequest => ({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + messages: [ + { role: "user", content: "run the tool" }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_1", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }, + ], + }); + + const adapter = anthropicAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + expect(stats).toStrictEqual({ + tokensBefore: boundaryTokens, + tokensAfter: boundaryTokens, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + expect(adapter.toProviderRequest()).toStrictEqual(makeRequest()); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/anthropic.ts b/platform/backend/src/routes/proxy/adapters/anthropic.ts index 1121641b641..9853cccca12 100644 --- a/platform/backend/src/routes/proxy/adapters/anthropic.ts +++ b/platform/backend/src/routes/proxy/adapters/anthropic.ts @@ -4,7 +4,6 @@ import { PROVIDER_BILLING_BLOCK_BODY, PROVIDER_BILLING_BLOCK_TITLE, } from "@archestra/shared"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import { anthropicWorkloadIdentity } from "@/clients/anthropic-workload-identity"; import { @@ -39,7 +38,7 @@ import { isImageTooLarge, isMcpImageBlock, } from "../utils/mcp-image"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ============================================================================= // TYPE ALIASES @@ -608,6 +607,10 @@ class AnthropicStreamAdapter { readonly provider = "anthropic" as const; readonly state: StreamAccumulatorState; + // SSE encoding of each state.rawToolCallEvents entry, cached at accumulation + // time. getRawToolCallEvents runs once per tool-call chunk, so serializing + // there would re-encode the whole history per chunk (O(k^2) per stream). + private serializedToolCallEvents: string[] = []; private toolUseBlockIndices = new Set(); private currentToolCallIndex = -1; // Highest content-block index actually forwarded to the client, so a refusal @@ -681,7 +684,7 @@ class AnthropicStreamAdapter arguments: "", }); // Store raw event for replay after policy approval - this.state.rawToolCallEvents.push(chunk); + this.accumulateRawToolCallEvent(chunk); isToolCallChunk = true; } else { // Everything except client tool calls (text, thinking, @@ -707,7 +710,7 @@ class AnthropicStreamAdapter chunk.delta.partial_json; } // Store raw event for replay after policy approval - this.state.rawToolCallEvents.push(chunk); + this.accumulateRawToolCallEvent(chunk); isToolCallChunk = true; } else { // input_json_delta outside a tool_use block belongs to a @@ -724,7 +727,7 @@ class AnthropicStreamAdapter sseData = `event: content_block_stop\ndata: ${JSON.stringify(chunk)}\n\n`; } else { // Store raw event for replay after policy approval - this.state.rawToolCallEvents.push(chunk); + this.accumulateRawToolCallEvent(chunk); isToolCallChunk = true; } break; @@ -782,10 +785,7 @@ class AnthropicStreamAdapter } getRawToolCallEvents(): string[] { - return this.state.rawToolCallEvents.map( - (event) => - `event: ${(event as { type: string }).type}\ndata: ${JSON.stringify(event)}\n\n`, - ); + return this.serializedToolCallEvents; } formatCompleteTextSSE(text: string): string[] { @@ -899,6 +899,13 @@ class AnthropicStreamAdapter }, }; } + + private accumulateRawToolCallEvent(chunk: AnthropicStreamChunk): void { + // Serialize before pushing so a throwing stringify can't desync the two arrays. + const serialized = `event: ${chunk.type}\ndata: ${JSON.stringify(chunk)}\n\n`; + this.state.rawToolCallEvents.push(chunk); + this.serializedToolCallEvents.push(serialized); + } } // ============================================================================= @@ -921,10 +928,48 @@ export async function convertToolResultsToToon( let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidates from BOTH content shapes first so the native + // unwrap→parse→encode transform runs once per request (batched, off the JS + // thread). Results are positional and reapplied via message/content/block + // locators kept on the TS side — tool_use_id is NOT unique (one tool_result + // can hold several text blocks), so it is carried for logging only. + type AnthropicMessageContentBlocks = Extract< + AnthropicMessages[number]["content"], + unknown[] + >; + type AnthropicToolResultBlock = Extract< + AnthropicMessageContentBlocks[number], + { type: "tool_result" } + >; + type AnthropicToolResultInnerBlocks = Extract< + AnthropicToolResultBlock["content"], + unknown[] + >; + type ToonCandidate = + | { + kind: "string"; + messageIndex: number; + blockIndex: number; + block: AnthropicToolResultBlock; + content: string; + } + | { + kind: "array-text"; + messageIndex: number; + blockIndex: number; + block: AnthropicToolResultBlock; + textIndex: number; + textBlock: Extract< + AnthropicToolResultInnerBlocks[number], + { type: "text" } + >; + text: string; + }; + const candidates: ToonCandidate[] = []; + messages.forEach((message, messageIndex) => { // Only process user messages with content arrays that contain tool_result blocks if (message.role === "user" && Array.isArray(message.content)) { - const updatedContent = message.content.map((contentBlock) => { + message.content.forEach((contentBlock, blockIndex) => { if (contentBlock.type === "tool_result" && !contentBlock.is_error) { toolResultCount++; logger.info( @@ -938,183 +983,212 @@ export async function convertToolResultsToToon( // Handle string content if (typeof contentBlock.content === "string") { - try { - // Unwrap any extra text block wrapping from clients - const unwrapped = unwrapToolContent(contentBlock.content); - const parsed = JSON.parse(unwrapped); - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); - - // Count tokens for before and after - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - // Always count tokens - totalTokensBefore += tokensBefore; - - // Only apply compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensAfter += tokensAfter; - - logger.info( - { - toolCallId: contentBlock.tool_use_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - toonPreview: compressed.substring(0, 150), - provider: "anthropic", - }, - "convertToolResultsToToon: compressed (string content)", - ); - logger.trace( - { - toolCallId: contentBlock.tool_use_id, - before: noncompressed, - after: compressed, - provider: "anthropic", - supposedToBeJson: parsed, - }, - "convertToolResultsToToon: before/after", - ); - - return { - ...contentBlock, - content: compressed, - }; - } - - // Compression not applied - count non-compressed tokens to track total tokens anyway - totalTokensAfter += tokensBefore; - logger.info( - { - toolCallId: contentBlock.tool_use_id, - tokensBefore, - tokensAfter, - provider: "anthropic", - }, - "Skipping TOON compression - compressed output has more tokens", - ); - return contentBlock; - } catch { - logger.info( - { - toolCallId: contentBlock.tool_use_id, - contentPreview: - typeof contentBlock.content === "string" - ? contentBlock.content.substring(0, 100) - : "non-string", - }, - "convertToolResultsToToon: skipping - string content is not JSON", - ); - return contentBlock; - } + candidates.push({ + kind: "string", + messageIndex, + blockIndex, + block: contentBlock, + content: contentBlock.content, + }); + return; } // Handle array content (content blocks format) if (Array.isArray(contentBlock.content)) { - const updatedBlocks = contentBlock.content.map((block) => { + contentBlock.content.forEach((block, textIndex) => { if (block.type === "text" && typeof block.text === "string") { - try { - // Unwrap any extra text block wrapping from clients - const unwrapped = unwrapToolContent(block.text); - // Try to parse as JSON - const parsed = JSON.parse(unwrapped); - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); - - // Count tokens for before and after - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - // Always count tokens - totalTokensBefore += tokensBefore; - - // Only apply compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensAfter += tokensAfter; - - logger.info( - { - toolCallId: contentBlock.tool_use_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - toonPreview: compressed.substring(0, 150), - }, - "convertToolResultsToToon: compressed (array content)", - ); - logger.trace( - { - toolCallId: contentBlock.tool_use_id, - before: noncompressed, - after: compressed, - provider: "anthropic", - supposedToBeJson: parsed, - }, - "convertToolResultsToToon: before/after", - ); - - return { - ...block, - text: compressed, - }; - } - - // Compression not applied - count non-compressed tokens to track total tokens anyway - totalTokensAfter += tokensBefore; - logger.info( - { - toolCallId: contentBlock.tool_use_id, - tokensBefore, - tokensAfter, - provider: "anthropic", - }, - "Skipping TOON compression - compressed output has more tokens", - ); - return block; - } catch { - // Not JSON, keep as-is - logger.info( - { - toolCallId: contentBlock.tool_use_id, - blockType: block.type, - textPreview: block.text?.substring(0, 100), - }, - "convertToolResultsToToon: skipping - content is not JSON", - ); - return block; - } + candidates.push({ + kind: "array-text", + messageIndex, + blockIndex, + block: contentBlock, + textIndex, + textBlock: block, + text: block.text, + }); } - return block; }); - - return { - ...contentBlock, - content: updatedBlocks, - }; } } - return contentBlock; }); + } + }); - return { - ...message, - content: updatedContent, + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.block.tool_use_id, + rawContent: + candidate.kind === "string" ? candidate.content : candidate.text, + unwrap: true, + })), + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: toolResultCount > 0, + skipReason: "addon_unavailable", + }, + }; + } + + const result = [...messages]; + // Clone-on-write: untouched messages/blocks keep their original objects. + const clonedMessageContent = new Map(); + const contentFor = (messageIndex: number) => { + let cloned = clonedMessageContent.get(messageIndex); + if (!cloned) { + const message = messages[messageIndex]; + cloned = [...(message.content as AnthropicMessageContentBlocks)]; + clonedMessageContent.set(messageIndex, cloned); + result[messageIndex] = { ...message, content: cloned }; + } + return cloned; + }; + // Several text blocks can live in ONE tool_result block; clone its inner + // content array once per POSITION (message index → block index) and reuse + // it. Keyed by position, not by the block object: the same object can be + // aliased at several positions and each occurrence needs its own clone. + const clonedToolResultContent = new Map< + number, + Map + >(); + const innerBlocksFor = (candidate: { + messageIndex: number; + blockIndex: number; + block: AnthropicToolResultBlock; + }) => { + let byBlockIndex = clonedToolResultContent.get(candidate.messageIndex); + if (!byBlockIndex) { + byBlockIndex = new Map(); + clonedToolResultContent.set(candidate.messageIndex, byBlockIndex); + } + let cloned = byBlockIndex.get(candidate.blockIndex); + if (!cloned) { + cloned = [...(candidate.block.content as AnthropicToolResultInnerBlocks)]; + byBlockIndex.set(candidate.blockIndex, cloned); + contentFor(candidate.messageIndex)[candidate.blockIndex] = { + ...candidate.block, + content: cloned, }; } + return cloned; + }; - return message; + candidates.forEach((candidate, candidateIndex) => { + const { normalized, encoded: compressed } = encodedResults[candidateIndex]; + const contentBlock = candidate.block; + + if (compressed === null) { + if (candidate.kind === "string") { + logger.info( + { + toolCallId: contentBlock.tool_use_id, + contentPreview: candidate.content.substring(0, 100), + }, + "convertToolResultsToToon: skipping - string content is not JSON", + ); + } else { + // Not JSON, keep as-is + logger.info( + { + toolCallId: contentBlock.tool_use_id, + blockType: candidate.textBlock.type, + textPreview: candidate.text.substring(0, 100), + }, + "convertToolResultsToToon: skipping - content is not JSON", + ); + } + return; + } + + // Token accounting on the normalized (unwrapped) string, exactly as + // before — each text block is counted individually. + const tokensBefore = tokenizer.countTokens([ + { role: "user", content: normalized }, + ]); + const tokensAfter = tokenizer.countTokens([ + { role: "user", content: compressed }, + ]); + + // Always count tokens + totalTokensBefore += tokensBefore; + + // Only apply compression if it actually saves tokens + if (tokensAfter < tokensBefore) { + totalTokensAfter += tokensAfter; + + if (candidate.kind === "string") { + logger.info( + { + toolCallId: contentBlock.tool_use_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + toonPreview: compressed.substring(0, 150), + provider: "anthropic", + }, + "convertToolResultsToToon: compressed (string content)", + ); + } else { + logger.info( + { + toolCallId: contentBlock.tool_use_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + toonPreview: compressed.substring(0, 150), + }, + "convertToolResultsToToon: compressed (array content)", + ); + } + logger.trace( + { + toolCallId: contentBlock.tool_use_id, + before: normalized, + after: compressed, + provider: "anthropic", + }, + "convertToolResultsToToon: before/after", + ); + + if (candidate.kind === "string") { + contentFor(candidate.messageIndex)[candidate.blockIndex] = { + ...contentBlock, + content: compressed, + }; + } else { + innerBlocksFor(candidate)[candidate.textIndex] = { + ...candidate.textBlock, + text: compressed, + }; + } + return; + } + + // Compression not applied - count non-compressed tokens to track total tokens anyway + totalTokensAfter += tokensBefore; + logger.info( + { + toolCallId: contentBlock.tool_use_id, + tokensBefore, + tokensAfter, + provider: "anthropic", + }, + "Skipping TOON compression - compressed output has more tokens", + ); }); logger.info( diff --git a/platform/backend/src/routes/proxy/adapters/bedrock-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/bedrock-toon-compression.test.ts new file mode 100644 index 00000000000..60510cf833e --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/bedrock-toon-compression.test.ts @@ -0,0 +1,457 @@ +// Pins the Bedrock adapter's TOON compression cutover to the native addon: +// full transformed-request exact equality (TOON content from the committed v3 +// golden corpus, everything else byte-equal). Bedrock-specific semantics +// pinned here: compression is applied UNCONDITIONALLY (no keep/reject — +// encoded tokens are always recorded, even when TOON is larger), NEITHER +// branch unwraps client wrappers, only content[0] of a toolResult is read, a +// compressed result replaces the WHOLE content array with one text item (the +// json branch is rewritten to text too), and error-status results are skipped +// entirely. Requires the built addon: mandatory in CI; locally it skips +// visibly — run `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Bedrock } from "@/types"; +import { bedrockAdapterFactory } from "./bedrock"; + +type BedrockRequest = Bedrock.Types.ConverseRequest; + +// Corpus picks: a uniform array (TOON smaller), malformed JSON (kept as-is), +// a json-branch twin of the uniform array, and a wrapped [{type:"text",...}] +// payload encoded WITHOUT unwrapping (its TOON is the wrapper array itself +// and is LARGER than the original under the Anthropic tokenizer — Bedrock +// still applies it). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const MALFORMED = corpusEntry("malformed-prose"); +const JSON_BRANCH = corpusEntry("bedrock-json-branch"); +const WRAPPED_NO_UNWRAP = corpusEntry("wrapped-but-unwrap-false"); +// A json-branch object whose TOON encoding is LARGER under the Anthropic +// tokenizer — Bedrock still applies it (unconditional apply on both branches). +const JSON_LARGER = corpusEntry("boundary-obj-3"); + +// Bedrock accounting uses the Anthropic tokenizer as an approximation. +const countTokens = makeCountTokens("anthropic"); +// The json branch tokenizes the adapter's own serialization of the json +// value — recompute it here instead of trusting the corpus rawContent. +const jsonBranchSerialized = JSON.stringify(JSON.parse(JSON_BRANCH.rawContent)); + +const BEDROCK_MODEL = "anthropic.claude-sonnet-4-5-20250929-v1:0"; + +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ + provider: "bedrock", + modelId: BEDROCK_MODEL, + }); + +describeNative("Bedrock adapter TOON compression (native addon)", () => { + test("transforms the full provider request exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): BedrockRequest => ({ + modelId: BEDROCK_MODEL, + inferenceConfig: { temperature: 0.25 }, + messages: [ + { + role: "user", + content: [{ text: "What files are in the current directory?" }], + }, + { + role: "assistant", + content: [ + { + toolUse: { + toolUseId: "tooluse_list", + name: "list_files", + input: { directory: "." }, + }, + }, + ], + }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_text", + content: [{ text: UNIFORM.rawContent }], + status: "success", + }, + }, + { + toolResult: { + toolUseId: "tooluse_json", + content: [{ json: JSON.parse(JSON_BRANCH.rawContent) }], + }, + }, + { + toolResult: { + toolUseId: "tooluse_malformed", + content: [{ text: MALFORMED.rawContent }], + }, + }, + ], + }, + ], + }); + + const adapter = bedrockAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + // Both the text and json branches are rewritten to a single text item + // (the whole content array is replaced and toolResult fields like + // `status` are preserved by the spread); malformed content is kept. + const expectedRequest = makeRequest(); + // biome-ignore lint/style/noNonNullAssertion: fixture shape is static + expectedRequest.messages![2].content = [ + { + toolResult: { + toolUseId: "tooluse_text", + content: [{ text: UNIFORM.expected.encoded as string }], + status: "success", + }, + }, + { + toolResult: { + toolUseId: "tooluse_json", + content: [{ text: JSON_BRANCH.expected.encoded as string }], + }, + }, + { + toolResult: { + toolUseId: "tooluse_malformed", + content: [{ text: MALFORMED.rawContent }], + }, + }, + ]; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + // Unconditional accounting: both branches count original serialization + // vs encoded; malformed content is not counted at all. + const tokensBefore = + countTokens(UNIFORM.rawContent) + countTokens(jsonBranchSerialized); + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(JSON_BRANCH.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right blocks when non-candidate blocks are interleaved", async () => { + await upsertPricing(); + + // Non-candidates between candidates: a plain text block, an error-status + // toolResult (whose content WOULD compress if wrongly collected), an + // empty-content toolResult, and a non-user assistant turn. The winning + // candidate sits at native index 1, message index 2, block index 0 — all + // three DIFFER, so applying the native result by any wrong index + // (candidate-as-message, candidate-as-block, ...) compresses the wrong + // block and fails the equality below. + const makeRequest = (): BedrockRequest => ({ + modelId: BEDROCK_MODEL, + messages: [ + { + role: "user", + content: [ + { text: "status update" }, + { + toolResult: { + toolUseId: "tooluse_error", + content: [{ text: UNIFORM.rawContent }], + status: "error", + }, + }, + { + toolResult: { + toolUseId: "tooluse_empty", + content: [], + }, + }, + { + // native index 0 — malformed, kept as-is + toolResult: { + toolUseId: "tooluse_malformed", + content: [{ text: MALFORMED.rawContent }], + }, + }, + ], + }, + { role: "assistant", content: [{ text: "Let me check." }] }, + { + role: "user", + content: [ + { + // native index 1 — the winner + toolResult: { + toolUseId: "tooluse_ok", + content: [{ text: UNIFORM.rawContent }], + }, + }, + ], + }, + ], + }); + + const adapter = bedrockAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + const expectedRequest = makeRequest(); + // biome-ignore lint/style/noNonNullAssertion: fixture shape is static + expectedRequest.messages![2].content[0] = { + toolResult: { + toolUseId: "tooluse_ok", + content: [{ text: UNIFORM.expected.encoded as string }], + }, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + const tokensBefore = countTokens(UNIFORM.rawContent); + const tokensAfter = countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("reads only content[0] and replaces the whole content array (multi-item content)", async () => { + await upsertPricing(); + + const makeRequest = (): BedrockRequest => ({ + modelId: BEDROCK_MODEL, + messages: [ + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_multi", + content: [ + { text: UNIFORM.rawContent }, + { text: "a second item that is silently dropped" }, + ], + }, + }, + { + toolResult: { + toolUseId: "tooluse_first_malformed", + content: [ + { text: MALFORMED.rawContent }, + // Compressible, but never looked at: content[0] decides. + { json: JSON.parse(JSON_BRANCH.rawContent) }, + ], + }, + }, + ], + }, + ], + }); + + const adapter = bedrockAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + const expectedRequest = makeRequest(); + // biome-ignore lint/style/noNonNullAssertion: fixture shape is static + expectedRequest.messages![0].content[0] = { + toolResult: { + toolUseId: "tooluse_multi", + // The whole multi-item content collapses to one text item. + content: [{ text: UNIFORM.expected.encoded as string }], + }, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + const tokensBefore = countTokens(UNIFORM.rawContent); + const tokensAfter = countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // Cases the combined request cannot isolate: hadToolResults is true even + // for malformed-only content (Bedrock counts every non-error toolResult), + // there is no reject rule on either branch, and error-status results are + // skipped entirely. + test("a lone malformed result: zeroed totals but hadToolResults=true, message untouched", async () => { + await upsertPricing(); + + const makeRequest = (): BedrockRequest => ({ + modelId: BEDROCK_MODEL, + messages: [ + { role: "user", content: [{ text: "run the tool" }] }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_1", + content: [{ text: MALFORMED.rawContent }], + }, + }, + ], + }, + ], + }); + + const adapter = bedrockAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + expect(adapter.toProviderRequest()).toStrictEqual(makeRequest()); + }); + + test("text branch unconditional apply (encoded larger, no unwrap, still applied)", async () => { + await upsertPricing(); + + const adapter = bedrockAdapterFactory.createRequestAdapter({ + modelId: BEDROCK_MODEL, + messages: [ + { role: "user", content: [{ text: "run the tool" }] }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_1", + content: [{ text: WRAPPED_NO_UNWRAP.rawContent }], + }, + }, + ], + }, + ], + }); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + const tokensBefore = countTokens(WRAPPED_NO_UNWRAP.rawContent); + const tokensAfter = countTokens( + WRAPPED_NO_UNWRAP.expected.encoded as string, + ); + expect(tokensAfter).toBeGreaterThan(tokensBefore); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_1", + content: [{ text: WRAPPED_NO_UNWRAP.expected.encoded as string }], + }, + }, + ], + }); + }); + + test("json branch unconditional apply (encoded larger, still applied)", async () => { + await upsertPricing(); + + const jsonLargerSerialized = JSON.stringify( + JSON.parse(JSON_LARGER.rawContent), + ); + const adapter = bedrockAdapterFactory.createRequestAdapter({ + modelId: BEDROCK_MODEL, + messages: [ + { role: "user", content: [{ text: "run the tool" }] }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_json_larger", + content: [{ json: JSON.parse(JSON_LARGER.rawContent) }], + }, + }, + ], + }, + ], + }); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + const tokensBefore = countTokens(jsonLargerSerialized); + const tokensAfter = countTokens(JSON_LARGER.expected.encoded as string); + expect(tokensAfter).toBeGreaterThan(tokensBefore); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + + // Still applied despite being larger — no keep/reject on this branch. + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_json_larger", + content: [{ text: JSON_LARGER.expected.encoded as string }], + }, + }, + ], + }); + }); + + test("error-status result is skipped entirely (not counted as a tool result)", async () => { + await upsertPricing(); + + const makeRequest = (): BedrockRequest => ({ + modelId: BEDROCK_MODEL, + messages: [ + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tooluse_error", + content: [{ text: UNIFORM.rawContent }], + status: "error", + }, + }, + ], + }, + ], + }); + + const adapter = bedrockAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression(BEDROCK_MODEL); + + expect(adapter.toProviderRequest()).toStrictEqual(makeRequest()); + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/bedrock.ts b/platform/backend/src/routes/proxy/adapters/bedrock.ts index 18091269a19..b00bbd8fd08 100644 --- a/platform/backend/src/routes/proxy/adapters/bedrock.ts +++ b/platform/backend/src/routes/proxy/adapters/bedrock.ts @@ -6,7 +6,6 @@ import { import type { ConverseStreamOutput } from "@aws-sdk/client-bedrock-runtime"; import { EventStreamCodec } from "@smithy/eventstream-codec"; import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; -import { encode as toonEncode } from "@toon-format/toon"; import { BedrockClient } from "@/clients/bedrock-client"; import { decodeBedrockSigV4Marker, @@ -35,6 +34,7 @@ import type { UsageView, } from "@/types"; import { extractCommonMessageText } from "@/types"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ToolCompressionStats imported from @/types @@ -972,6 +972,12 @@ class BedrockStreamAdapter { readonly provider = "bedrock" as const; readonly state: StreamAccumulatorState; + // Event-stream encoding of each accumulated tool-call event (tool names + // already decoded) and of the buffered final events, cached at accumulation + // time. getRawToolCallEvents runs once per tool-call chunk, so encoding + // there would re-encode the whole history per chunk (O(k^2) per stream). + private encodedToolCallEvents: Uint8Array[] = []; + private encodedPendingFinalEvents: Uint8Array[] = []; private currentToolCallIndex = -1; private toolNameMapping: ToolNameMapping = createEmptyToolNameMapping(); // Set to the refusal text when the streamed response was replaced by a policy @@ -985,9 +991,6 @@ class BedrockStreamAdapter private bedrockState: { latencyMs: number | null; trace: unknown | null; - // Buffer for messageStop and metadata events when tool calls are pending - // These must be sent AFTER tool call events in the correct stream order - pendingFinalEvents: BedrockStreamEventWithRaw[]; }; constructor() { @@ -1007,7 +1010,6 @@ class BedrockStreamAdapter this.bedrockState = { latencyMs: null, trace: null, - pendingFinalEvents: [], }; } @@ -1045,13 +1047,28 @@ class BedrockStreamAdapter ) { // Tool use block - buffer for policy evaluation const toolUse = blockStart.start.toolUse; + const decodedName = decodeToolName( + toolUse.name ?? "", + this.toolNameMapping, + ); this.currentToolCallIndex = this.state.toolCalls.length; this.state.toolCalls.push({ id: toolUse.toolUseId ?? "", - name: decodeToolName(toolUse.name ?? "", this.toolNameMapping), + name: decodedName, arguments: "", }); + // Re-encode with the decoded tool name for replay; the raw bytes + // contain the encoded name (hyphens replaced with underscores). + // Encode before pushing so a throwing encoder can't desync the arrays. + const encodedBlockStart = encodeEventStreamMessage( + "contentBlockStart", + { + ...blockStart, + start: { toolUse: { ...toolUse, name: decodedName } }, + }, + ); this.state.rawToolCallEvents.push(chunk); + this.encodedToolCallEvents.push(encodedBlockStart); isToolCallChunk = true; } else { sseData = @@ -1086,7 +1103,12 @@ class BedrockStreamAdapter this.state.toolCalls[this.currentToolCallIndex].arguments += toolUseDelta.input; } + const encodedBlockDelta = encodeEventStreamMessage( + "contentBlockDelta", + chunk.contentBlockDelta, + ); this.state.rawToolCallEvents.push(chunk); + this.encodedToolCallEvents.push(encodedBlockDelta); isToolCallChunk = true; } } else if ("contentBlockStop" in chunk && chunk.contentBlockStop) { @@ -1095,7 +1117,12 @@ class BedrockStreamAdapter this.currentToolCallIndex === this.state.toolCalls.length - 1; if (isToolBlock) { + const encodedBlockStop = encodeEventStreamMessage( + "contentBlockStop", + chunk.contentBlockStop, + ); this.state.rawToolCallEvents.push(chunk); + this.encodedToolCallEvents.push(encodedBlockStop); isToolCallChunk = true; } else { sseData = @@ -1107,7 +1134,12 @@ class BedrockStreamAdapter // If we have pending tool calls, buffer this event to send after tool blocks // The stream order must be: text blocks → tool blocks → messageStop → metadata if (this.state.toolCalls.length > 0) { - this.bedrockState.pendingFinalEvents.push(chunk); + // Buffer for replay after the tool events; raw bytes are safe here + // because final events carry no tool names. + this.encodedPendingFinalEvents.push( + rawBytes ?? + encodeEventStreamMessage("messageStop", chunk.messageStop), + ); isToolCallChunk = true; // Mark as tool-related so it's not streamed yet } else { sseData = @@ -1144,7 +1176,10 @@ class BedrockStreamAdapter } // If we have pending tool calls, buffer this event to send after tool blocks if (this.state.toolCalls.length > 0) { - this.bedrockState.pendingFinalEvents.push(chunk); + // Raw bytes are safe here: final events carry no tool names. + this.encodedPendingFinalEvents.push( + rawBytes ?? encodeEventStreamMessage("metadata", chunk.metadata), + ); isToolCallChunk = true; // Mark as tool-related so it's not streamed yet } else { // Pass through metadata chunk as-is - this is the final event @@ -1237,80 +1272,11 @@ class BedrockStreamAdapter } getRawToolCallEvents(): Uint8Array[] { - const result: Uint8Array[] = []; - - // Re-encode all tool call content blocks with decoded tool names - // We cannot use raw bytes because they contain encoded names (hyphens replaced with underscores) - for (const rawEvent of this.state.rawToolCallEvents) { - const event = rawEvent as BedrockStreamEventWithRaw; - - if ("contentBlockStart" in event && event.contentBlockStart) { - const blockStart = event.contentBlockStart; - // Decode tool name if this is a tool use block - if ( - blockStart.start && - "toolUse" in blockStart.start && - blockStart.start.toolUse - ) { - const originalName = blockStart.start.toolUse.name ?? ""; - const decodedName = decodeToolName( - originalName, - this.toolNameMapping, - ); - const decodedEvent = { - ...blockStart, - start: { - toolUse: { - ...blockStart.start.toolUse, - name: decodedName, - }, - }, - }; - result.push( - encodeEventStreamMessage("contentBlockStart", decodedEvent), - ); - } else { - result.push( - encodeEventStreamMessage( - "contentBlockStart", - event.contentBlockStart, - ), - ); - } - } else if ("contentBlockDelta" in event && event.contentBlockDelta) { - result.push( - encodeEventStreamMessage( - "contentBlockDelta", - event.contentBlockDelta, - ), - ); - } else if ("contentBlockStop" in event && event.contentBlockStop) { - result.push( - encodeEventStreamMessage("contentBlockStop", event.contentBlockStop), - ); - } - } - - // Then, add the buffered final events (messageStop and metadata) in order - // These must come AFTER all content blocks for correct stream order - for (const finalEvent of this.bedrockState.pendingFinalEvents) { - const event = finalEvent as BedrockStreamEventWithRaw; - - // Use original raw bytes if available (these don't contain tool names) - if (event.__rawBytes) { - result.push(event.__rawBytes); - continue; - } - - // Fallback to re-encoding - if ("messageStop" in event && event.messageStop) { - result.push(encodeEventStreamMessage("messageStop", event.messageStop)); - } else if ("metadata" in event && event.metadata) { - result.push(encodeEventStreamMessage("metadata", event.metadata)); - } - } - - return result; + // Both caches are encoded at accumulation time. The buffered final events + // (messageStop and metadata) must come AFTER all content blocks for + // correct stream order; Bedrock emits them last, so appending the second + // cache preserves arrival order and keeps indices stable across calls. + return [...this.encodedToolCallEvents, ...this.encodedPendingFinalEvents]; } formatCompleteTextSSE(text: string): Uint8Array[] { @@ -1460,10 +1426,27 @@ export async function convertToolResultsToToon( let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidate tool results first so the native parse→encode transform + // runs once per request (batched, off the JS thread); results are positional + // and reapplied via message/content-block locators. Preserved semantics: + // only content[0] is read, a compressed result replaces the WHOLE content + // array, neither branch unwraps client wrappers (unwrap: false), and + // compression is applied unconditionally (no keep/reject rule). + type BedrockToolResult = Extract< + Bedrock.Types.Message["content"][number], + { toolResult: unknown } + >["toolResult"]; + const candidates: { + messageIndex: number; + blockIndex: number; + branch: "text" | "json"; + toolResult: BedrockToolResult; + rawContent: string; + }[] = []; + messages.forEach((message, messageIndex) => { // Only process user messages with content arrays that contain tool_result blocks if (message.role === "user" && Array.isArray(message.content)) { - const updatedContent = message.content.map((contentBlock) => { + message.content.forEach((contentBlock, blockIndex) => { if ( isToolResultBlock(contentBlock) && contentBlock.toolResult.status !== "error" @@ -1479,85 +1462,131 @@ export async function convertToolResultsToToon( "text" in firstContent && typeof firstContent.text === "string" ) { - try { - const parsed = JSON.parse(firstContent.text); - const noncompressed = firstContent.text; - const compressed = toonEncode(parsed); - - // Count tokens for before and after - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - totalTokensBefore += tokensBefore; - totalTokensAfter += tokensAfter; - - logger.info( - { - toolUseId: toolResult.toolUseId, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - provider: "bedrock", - }, - "convertToolResultsToToon: compressed", - ); - - return { - toolResult: { - ...toolResult, - content: [{ text: compressed }], - }, - }; - } catch { - logger.info( - { - toolUseId: toolResult.toolUseId, - }, - "convertToolResultsToToon: skipping - content is not JSON", - ); - return contentBlock; - } + candidates.push({ + messageIndex, + blockIndex, + branch: "text", + toolResult, + rawContent: firstContent.text, + }); } else if ("json" in firstContent && firstContent.json) { try { - const noncompressed = JSON.stringify(firstContent.json); - const compressed = toonEncode(firstContent.json); - - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - totalTokensBefore += tokensBefore; - totalTokensAfter += tokensAfter; - - return { - toolResult: { - ...toolResult, - content: [{ text: compressed }], - }, - }; + // JSON.stringify is typed as returning string but can yield + // undefined at runtime (e.g. a toJSON returning undefined); + // the old TS path then TOON-encoded the value as "null" and + // continued. Keep that, and never let a non-string poison the + // whole native batch. + const serialized: string | undefined = JSON.stringify( + firstContent.json, + ); + candidates.push({ + messageIndex, + blockIndex, + branch: "json", + toolResult, + rawContent: + typeof serialized === "string" ? serialized : "null", + }); } catch { - return contentBlock; + // Unstringifiable json content is kept as-is (silently, as before). } } } } - return contentBlock; }); + } + }); - return { - ...message, - content: updatedContent, - }; + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.toolResult.toolUseId, + rawContent: candidate.rawContent, + unwrap: false, + })), + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: toolResultCount > 0, + skipReason: "addon_unavailable", + }, + }; + } + + const result = [...messages]; + // Clone a message's content array on first write so untouched messages keep + // their original objects. + const clonedContent = new Map(); + const contentFor = (messageIndex: number) => { + let cloned = clonedContent.get(messageIndex); + if (!cloned) { + const message = messages[messageIndex]; + cloned = [...message.content]; + clonedContent.set(messageIndex, cloned); + result[messageIndex] = { ...message, content: cloned }; } + return cloned; + }; - return message; - }) as BedrockMessages; + candidates.forEach((candidate, candidateIndex) => { + const { encoded: compressed } = encodedResults[candidateIndex]; + const { toolResult, rawContent } = candidate; + + if (compressed === null) { + if (candidate.branch === "text") { + logger.info( + { + toolUseId: toolResult.toolUseId, + }, + "convertToolResultsToToon: skipping - content is not JSON", + ); + } + // json branch: kept as-is silently, as before. + return; + } + + // Token accounting on the original serialization (no unwrap on either branch). + const tokensBefore = tokenizer.countTokens([ + { role: "user", content: rawContent }, + ]); + const tokensAfter = tokenizer.countTokens([ + { role: "user", content: compressed }, + ]); + totalTokensBefore += tokensBefore; + totalTokensAfter += tokensAfter; + + if (candidate.branch === "text") { + logger.info( + { + toolUseId: toolResult.toolUseId, + beforeLength: rawContent.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + provider: "bedrock", + }, + "convertToolResultsToToon: compressed", + ); + } + + contentFor(candidate.messageIndex)[candidate.blockIndex] = { + toolResult: { + ...toolResult, + content: [{ text: compressed }], + }, + }; + }); logger.info( { messageCount: messages.length, toolResultCount }, diff --git a/platform/backend/src/routes/proxy/adapters/cohere-openai.ts b/platform/backend/src/routes/proxy/adapters/cohere-openai.ts index 1784e2585bb..e63661c59f4 100644 --- a/platform/backend/src/routes/proxy/adapters/cohere-openai.ts +++ b/platform/backend/src/routes/proxy/adapters/cohere-openai.ts @@ -139,6 +139,12 @@ class CohereOpenaiStreamAdapter } getRawToolCallEvents(): string[] { + // Known non-conforming (ledger: tool-events-untranslated-empty-history): + // this wrapper never translates Cohere tool-call events to OpenAI SSE, so + // model-router Cohere tool calls are never streamed or flushed to clients. + // Implementing that translation is new behavior tracked separately, not a + // replay-contract fix, so this adapter is exempt from the full-history + // contract documented on LLMStreamAdapter.getRawToolCallEvents. return []; } diff --git a/platform/backend/src/routes/proxy/adapters/cohere-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/cohere-toon-compression.test.ts new file mode 100644 index 00000000000..9b71b9e216d --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/cohere-toon-compression.test.ts @@ -0,0 +1,216 @@ +// Pins the Cohere adapter's TOON compression cutover to the native addon: +// full transformed-messages exact equality (TOON content from the committed +// v3 golden corpus, everything else byte-equal) plus the Cohere-specific +// wins-only accounting the combined request cannot isolate: totals are +// updated ONLY on wins — a rejected (not smaller) or malformed payload +// contributes nothing to either total, so hadToolResults (totalTokensBefore > +// 0) is false when no result compresses. Requires the built addon: mandatory +// in CI; locally it skips visibly — run `pnpm test:native` from +// platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + makeToolCall, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Cohere } from "@/types"; +import { cohereAdapterFactory } from "./cohere"; + +type CohereRequest = Cohere.Types.ChatRequest; + +// Corpus picks: a uniform array (compression wins), a wrapped +// [{type:"text",...}] payload (unwrapped, compression wins), malformed JSON +// (kept as-is), and a near-boundary object whose TOON encoding does not save +// tokens under the Cohere tokenizer (rejected — and, per the wins-only rule, +// not counted at all). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const MALFORMED = corpusEntry("malformed-prose"); +const NEAR_BOUNDARY = corpusEntry("boundary-obj-1"); + +const countTokens = makeCountTokens("cohere"); +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ + provider: "cohere", + modelId: "command-r-plus", + }); + +describeNative("Cohere adapter TOON compression (native addon)", () => { + test("transforms the full messages exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): CohereRequest => ({ + model: "command-r-plus", + temperature: 0.25, + messages: [ + { role: "system", content: "You are a filesystem assistant." }, + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + tool_calls: [ + makeToolCall("call_uniform", "list_files"), + makeToolCall("call_wrapped", "read_wrapped"), + makeToolCall("call_malformed", "read_notes"), + makeToolCall("call_boundary", "read_config"), + ], + }, + { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_malformed", + content: MALFORMED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }); + + const adapter = cohereAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("command-r-plus"); + + const expectedMessages = makeRequest().messages; + // Compression wins for the uniform array and the wrapped payload; the + // malformed and near-boundary results keep their original content. + expectedMessages[3] = { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.expected.encoded as string, + }; + expectedMessages[4] = { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.expected.encoded as string, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedMessages); + + // Wins-only accounting: the rejected near-boundary payload contributes + // NOTHING to either total (unlike the OpenAI-family rule); malformed + // content is not counted either. + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized); + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right candidates when non-candidate messages are interleaved", async () => { + await upsertPricing(); + + // Cohere tool messages are string-only, so non-candidates here are + // assistant/user messages between tool messages: the native result index + // diverges from the message index — off-by-one positional application + // would compress the wrong message and fail the equality below. + const makeRequest = (): CohereRequest => ({ + model: "command-r-plus", + messages: [ + { role: "user", content: "Inspect the workspace." }, + { + role: "assistant", + tool_calls: [makeToolCall("call_a", "read_config")], + }, + { + role: "tool", + tool_call_id: "call_a", + content: NEAR_BOUNDARY.rawContent, + }, + { role: "assistant", content: "Let me look further." }, + { + role: "assistant", + tool_calls: [ + makeToolCall("call_b", "list_files"), + makeToolCall("call_c", "read_notes"), + ], + }, + { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_c", + content: MALFORMED.rawContent, + }, + ], + }); + + const adapter = cohereAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("command-r-plus"); + + // Only B compresses; A is rejected (near-boundary), C is malformed. + const expectedMessages = makeRequest().messages; + expectedMessages[5] = { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.expected.encoded as string, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedMessages); + + const tokensBefore = countTokens(UNIFORM.expected.normalized); + const tokensAfter = countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // Wins-only accounting isolated: with no win, NOTHING is counted — even a + // parseable rejected payload — so hadToolResults is false for both cases. + for (const [row, entry] of [ + ["malformed", MALFORMED], + ["rejected (not smaller)", NEAR_BOUNDARY], + ] as const) { + test(`a lone ${row} result: nothing counted, hadToolResults=false, message untouched`, async () => { + await upsertPricing(); + + const adapter = cohereAdapterFactory.createRequestAdapter({ + model: "command-r-plus", + messages: [ + { role: "user", content: "run the tool" }, + { role: "tool", tool_call_id: "call_1", content: entry.rawContent }, + ], + }); + const stats = await adapter.applyToonCompression("command-r-plus"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: entry.rawContent, + }); + }); + } +}); diff --git a/platform/backend/src/routes/proxy/adapters/cohere.ts b/platform/backend/src/routes/proxy/adapters/cohere.ts index d023c06ba84..091682c968c 100644 --- a/platform/backend/src/routes/proxy/adapters/cohere.ts +++ b/platform/backend/src/routes/proxy/adapters/cohere.ts @@ -1,12 +1,10 @@ import { randomUUID } from "node:crypto"; import type { ArchestraInternalErrorCode } from "@archestra/shared"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import config from "@/config"; import logger from "@/logging"; import { ModelModel } from "@/models"; import { metrics } from "@/observability"; -import { getTokenizer } from "@/tokenizers"; import type { ChunkProcessingResult, Cohere, @@ -25,7 +23,7 @@ import type { } from "@/types"; import { extractCommonMessageText } from "@/types"; import type { ToolCompressionStats as CompressionStats } from "../utils/toon-conversion"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ============================================================================= // TYPE ALIASES @@ -737,89 +735,117 @@ export async function convertToolResultsToToon( messages: CohereMessages; stats: CompressionStats; }> { - const tokenizer = getTokenizer("cohere"); - let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidate tool messages first so the native unwrap→parse→encode + // transform runs once per request (batched, off the JS thread); results are + // positional and reapplied by message index. + const candidates: { + index: number; + message: Cohere.Types.ToolMessage; + }[] = []; + messages.forEach((message, index) => { if (message.role === "tool") { - const toolMsg = message as Cohere.Types.ToolMessage; + candidates.push({ index, message: message as Cohere.Types.ToolMessage }); + } + }); - try { - const unwrapped = unwrapToolContent(toolMsg.content); - const parsedRes = safeJsonParse(unwrapped); - if (!parsedRes.ok) { - logger.info( - { - toolCallId: toolMsg.tool_call_id, - contentPreview: toolMsg.content.substring(0, 100), - }, - "convertToolResultsToToon: skipping - content is not JSON", - ); - return message; - } + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.message.tool_call_id, + rawContent: candidate.message.content, + unwrap: true, + })), + "normalized", + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: candidates.length > 0, + skipReason: "addon_unavailable", + }, + }; + } - const parsed = parsedRes.value as unknown; - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); + const result = [...messages]; + candidates.forEach((candidate, candidateIndex) => { + const { + normalized, + encoded: compressed, + beforeTokens, + encodedTokens, + } = encodedResults[candidateIndex]; + const { message: toolMsg } = candidate; + + // Counts come from the native pass (on the "normalized" baseline). Present + // iff the item was encoded; a null compressed output means non-JSON content. + if ( + compressed === null || + beforeTokens === null || + encodedTokens === null + ) { + logger.info( + { + toolCallId: toolMsg.tool_call_id, + contentPreview: toolMsg.content.substring(0, 100), + }, + "convertToolResultsToToon: skipping - content is not JSON", + ); + return; + } - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); + const tokensBefore = beforeTokens; + const tokensAfter = encodedTokens; - // Only use TOON compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensBefore += tokensBefore; - totalTokensAfter += tokensAfter; + // Only use TOON compression if it actually saves tokens — Cohere counts + // totals only for wins. + if (tokensAfter < tokensBefore) { + totalTokensBefore += tokensBefore; + totalTokensAfter += tokensAfter; - logger.info( - { - toolCallId: toolMsg.tool_call_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - tokensSaved: tokensBefore - tokensAfter, - provider: "cohere", - }, - "convertToolResultsToToon: compressed", - ); + logger.info( + { + toolCallId: toolMsg.tool_call_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + tokensSaved: tokensBefore - tokensAfter, + provider: "cohere", + }, + "convertToolResultsToToon: compressed", + ); - return { - ...toolMsg, - content: compressed, - }; - } else { - logger.info( - { - toolCallId: toolMsg.tool_call_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - tokensDiff: tokensAfter - tokensBefore, - provider: "cohere", - }, - "convertToolResultsToToon: skipping - compression increases tokens", - ); - return message; - } - } catch { - logger.info( - { - toolCallId: toolMsg.tool_call_id, - contentPreview: toolMsg.content.substring(0, 100), - }, - "convertToolResultsToToon: skipping - content is not JSON", - ); - return message; - } + result[candidate.index] = { + ...toolMsg, + content: compressed, + }; + } else { + logger.info( + { + toolCallId: toolMsg.tool_call_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + tokensDiff: tokensAfter - tokensBefore, + provider: "cohere", + }, + "convertToolResultsToToon: skipping - compression increases tokens", + ); } - return message; }); // Calculate cost savings diff --git a/platform/backend/src/routes/proxy/adapters/gemini-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/gemini-toon-compression.test.ts new file mode 100644 index 00000000000..906f0297987 --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/gemini-toon-compression.test.ts @@ -0,0 +1,282 @@ +// Pins the Gemini adapter's TOON compression cutover to the native addon: +// full transformed-contents exact equality (TOON content from the committed +// v3 golden corpus, everything else byte-equal). Gemini-specific semantics +// pinned here: the adapter serializes functionResponse.response itself and +// tokenizes that ORIGINAL serialization (not the unwrapped string) while +// parsing goes through the unwrap path, a winning part is replaced with +// { functionResponse: { ..., response: { tool_result: "" } } }, rejected +// payloads count their original tokens in both totals, and hadToolResults +// reflects every functionResponse part (even unparseable ones). Requires the +// built addon: mandatory in CI; locally it skips visibly — run +// `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + type GoldenEntry, + makeCountTokens, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Gemini } from "@/types"; +import { geminiAdapterFactory } from "./gemini"; + +type GeminiRequest = Gemini.Types.GenerateContentRequest; + +// Corpus picks: a uniform array (compression wins), a wrapped +// [{type:"text",...}] payload (unwrapped for parsing, compression wins), a +// wrapper whose inner text is NOT JSON (the "cannot be compressed" path — a +// response object always serializes to valid JSON, so this is Gemini's only +// unparseable case), and a near-boundary object whose TOON encoding does not +// save tokens under the Gemini tokenizer (rejected). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const UNPARSEABLE = corpusEntry("wrapped-text-not-json"); +const NEAR_BOUNDARY = corpusEntry("boundary-obj-1"); + +const countTokens = makeCountTokens("gemini"); +// Gemini accounting uses the adapter's own serialization of the response +// object — recompute it here instead of trusting the corpus rawContent. +const serialized = (entry: GoldenEntry) => + JSON.stringify(JSON.parse(entry.rawContent)); + +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ + provider: "gemini", + modelId: "gemini-2.0-flash", + }); + +describeNative("Gemini adapter TOON compression (native addon)", () => { + test("transforms the full contents exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): GeminiRequest => ({ + contents: [ + { + role: "user", + parts: [{ text: "What files are in the current directory?" }], + }, + { role: "model", parts: [{ text: "Calling the tools now." }] }, + { + role: "user", + parts: [ + { + functionResponse: { + id: "fc_uniform", + name: "list_files", + response: JSON.parse(UNIFORM.rawContent), + }, + }, + { + functionResponse: { + name: "read_wrapped", + response: JSON.parse(WRAPPED.rawContent), + }, + }, + { + functionResponse: { + name: "read_notes", + response: JSON.parse(UNPARSEABLE.rawContent), + }, + }, + { + functionResponse: { + name: "read_config", + response: JSON.parse(NEAR_BOUNDARY.rawContent), + }, + }, + ], + }, + ], + }); + + const adapter = geminiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("gemini-2.0-flash"); + + // Compression wins for the uniform array and the wrapped payload (the + // functionResponse spread keeps `id`); the unparseable and near-boundary + // responses keep their original parts. + const expectedContents = makeRequest().contents; + expectedContents[2].parts[0] = { + functionResponse: { + id: "fc_uniform", + name: "list_files", + response: { tool_result: UNIFORM.expected.encoded as string }, + }, + }; + expectedContents[2].parts[1] = { + functionResponse: { + name: "read_wrapped", + response: { tool_result: WRAPPED.expected.encoded as string }, + }, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedContents); + + // Token accounting uses the ORIGINAL serialization of each response — + // for the wrapped payload that is the whole wrapper array, not the + // unwrapped inner text (which would count fewer tokens). Rejected + // payloads count their original tokens in both totals; the unparseable + // response is not counted at all. + const boundaryTokens = countTokens(serialized(NEAR_BOUNDARY)); + const tokensBefore = + countTokens(serialized(UNIFORM)) + + countTokens(serialized(WRAPPED)) + + boundaryTokens; + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string) + + boundaryTokens; + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right parts when non-candidate parts are interleaved", async () => { + await upsertPricing(); + + // Text parts and a model turn between functionResponse parts: the + // winning candidate sits at native index 2, content index 3, part index + // 1 — all three DIFFER, so applying the native result by any wrong index + // (candidate-as-part, candidate-as-content, ...) compresses the wrong + // part and fails the equality below. + const makeRequest = (): GeminiRequest => ({ + contents: [ + { role: "user", parts: [{ text: "Inspect the workspace." }] }, + { + role: "user", + parts: [ + { text: "intermediate status" }, + { + // native index 0 + functionResponse: { + name: "read_config", + response: JSON.parse(NEAR_BOUNDARY.rawContent), + }, + }, + { + // native index 1 + functionResponse: { + name: "read_notes", + response: JSON.parse(UNPARSEABLE.rawContent), + }, + }, + ], + }, + { role: "model", parts: [{ text: "Let me look further." }] }, + { + role: "user", + parts: [ + { text: "more status" }, + { + // native index 2 — the winner + functionResponse: { + name: "list_files", + response: JSON.parse(UNIFORM.rawContent), + }, + }, + ], + }, + ], + }); + + const adapter = geminiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("gemini-2.0-flash"); + + // Only the uniform response compresses; the near-boundary and + // unparseable responses and the text parts are untouched. + const expectedContents = makeRequest().contents; + expectedContents[3].parts[1] = { + functionResponse: { + name: "list_files", + response: { tool_result: UNIFORM.expected.encoded as string }, + }, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedContents); + + const boundaryTokens = countTokens(serialized(NEAR_BOUNDARY)); + const tokensBefore = boundaryTokens + countTokens(serialized(UNIFORM)); + const tokensAfter = + boundaryTokens + countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // The case the combined request cannot isolate: hadToolResults is true even + // when the only functionResponse is unparseable after unwrap — Gemini + // counts every functionResponse part it sees. + test("a lone unparseable response: zeroed totals but hadToolResults=true, contents untouched", async () => { + await upsertPricing(); + + const makeContents = (): GeminiRequest["contents"] => [ + { role: "user", parts: [{ text: "run the tool" }] }, + { + role: "user", + parts: [ + { + functionResponse: { + name: "the_tool", + response: JSON.parse(UNPARSEABLE.rawContent), + }, + }, + ], + }, + ]; + + const adapter = geminiAdapterFactory.createRequestAdapter({ + contents: makeContents(), + }); + const stats = await adapter.applyToonCompression("gemini-2.0-flash"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + expect(adapter.getProviderMessages()).toStrictEqual(makeContents()); + }); + + test("a lone rejected response: original counted in both totals, wasEffective=false", async () => { + await upsertPricing(); + + const makeContents = (): GeminiRequest["contents"] => [ + { role: "user", parts: [{ text: "run the tool" }] }, + { + role: "user", + parts: [ + { + functionResponse: { + name: "the_tool", + response: JSON.parse(NEAR_BOUNDARY.rawContent), + }, + }, + ], + }, + ]; + + const adapter = geminiAdapterFactory.createRequestAdapter({ + contents: makeContents(), + }); + const stats = await adapter.applyToonCompression("gemini-2.0-flash"); + + const boundaryTokens = countTokens(serialized(NEAR_BOUNDARY)); + expect(stats).toStrictEqual({ + tokensBefore: boundaryTokens, + tokensAfter: boundaryTokens, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + expect(adapter.getProviderMessages()).toStrictEqual(makeContents()); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/gemini.ts b/platform/backend/src/routes/proxy/adapters/gemini.ts index ea4dee531c8..455c8f43d81 100644 --- a/platform/backend/src/routes/proxy/adapters/gemini.ts +++ b/platform/backend/src/routes/proxy/adapters/gemini.ts @@ -10,14 +10,12 @@ import { type HarmProbability, type Part, } from "@google/genai"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import { createGoogleGenAIClient } from "@/clients/gemini-client"; import config from "@/config"; import logger from "@/logging"; import { ModelModel } from "@/models"; import { metrics } from "@/observability"; -import { getTokenizer } from "@/tokenizers"; import type { ChunkProcessingResult, CommonMcpToolDefinition, @@ -40,7 +38,7 @@ import { isImageTooLarge, isMcpImageBlock, } from "../utils/mcp-image"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; import { sanitizeGeminiToolSchema } from "./gemini-schema"; // ============================================================================= @@ -867,15 +865,29 @@ async function convertToolResultsToToon( contents: GeminiContents; stats: ToolCompressionStats; }> { - const tokenizer = getTokenizer("gemini"); let toolResultCount = 0; let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = contents.map((content) => { + // Collect candidate functionResponse parts first so the native + // unwrap→parse→encode transform runs once per request (batched, off the JS + // thread); results are positional and reapplied via content/part locators. + // Gemini serializes the response object itself and tokenizes THAT original + // serialization (rawContent), while parsing goes through the unwrap path. + type GeminiFunctionResponsePart = Extract< + GeminiContents[number]["parts"][number], + { functionResponse: unknown } + >; + const candidates: { + contentIndex: number; + partIndex: number; + functionResponse: GeminiFunctionResponsePart["functionResponse"]; + rawContent: string; + }[] = []; + contents.forEach((content, contentIndex) => { // Only process user messages with parts containing functionResponse if (content.role === "user" && content.parts) { - const updatedParts = content.parts.map((part) => { + content.parts.forEach((part, partIndex) => { // Check if this part has a functionResponse if ( "functionResponse" in part && @@ -898,85 +910,24 @@ async function convertToolResultsToToon( // Handle response object - try to compress it const response = functionResponse.response; if (response && typeof response === "object") { + // JSON.stringify is typed as returning string but can throw or + // yield undefined at runtime (e.g. a toJSON returning undefined); + // a non-string must never reach the native binding — it would + // reject the WHOLE batch. Skip just this part, as before. + let rawContent: string | undefined; try { - const noncompressed = JSON.stringify(response); - const unwrapped = unwrapToolContent(noncompressed); - const parsed = JSON.parse(unwrapped); - const compressed = toonEncode(parsed); - - // Count tokens for before and after - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - // Always count tokens - totalTokensBefore += tokensBefore; - - // Only apply compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensAfter += tokensAfter; - - logger.info( - { - functionName: - "name" in functionResponse - ? functionResponse.name - : "unknown", - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - toonPreview: compressed.substring(0, 150), - provider: "gemini", - }, - "convertToolResultsToToon: compressed", - ); - logger.trace( - { - functionName: - "name" in functionResponse - ? functionResponse.name - : "unknown", - before: noncompressed, - after: compressed, - provider: "gemini", - }, - "convertToolResultsToToon: before/after", - ); - - // Return updated part with compressed response - return { - functionResponse: { - ...functionResponse, - // Gemini expects response as Record, but we now have a TOON string - // We wrap it in a {"tool_result": ""} object to match the expected format - response: { tool_result: compressed } as Record< - string, - unknown - >, - }, - }; - } - - // Compression not applied - count non-compressed tokens to track total tokens anyway - totalTokensAfter += tokensBefore; - logger.info( - { - functionName: - "name" in functionResponse - ? functionResponse.name - : "unknown", - tokensBefore, - tokensAfter, - provider: "gemini", - }, - "Skipping TOON compression - compressed output has more tokens", - ); - return part; + rawContent = JSON.stringify(response); } catch { + rawContent = undefined; + } + if (typeof rawContent === "string") { + candidates.push({ + contentIndex, + partIndex, + functionResponse, + rawContent, + }); + } else { logger.info( { functionName: @@ -986,20 +937,141 @@ async function convertToolResultsToToon( }, "convertToolResultsToToon: skipping - response cannot be compressed", ); - return part; } } } - return part; }); + } + }); - return { - ...content, - parts: updatedParts, + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: + "name" in candidate.functionResponse && + typeof candidate.functionResponse.name === "string" + ? candidate.functionResponse.name + : "unknown", + rawContent: candidate.rawContent, + unwrap: true, + })), + "raw", + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every content entry + // uncompressed and surface the explicit skip reason instead of + // fabricating stats. + return { + contents, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: toolResultCount > 0, + skipReason: "addon_unavailable", + }, + }; + } + + const result = [...contents]; + // Clone a content entry's parts array on first write so untouched entries + // keep their original objects. + const clonedParts = new Map(); + const partsFor = (contentIndex: number) => { + let cloned = clonedParts.get(contentIndex); + if (!cloned) { + const content = contents[contentIndex]; + cloned = [...content.parts]; + clonedParts.set(contentIndex, cloned); + result[contentIndex] = { ...content, parts: cloned }; + } + return cloned; + }; + + candidates.forEach((candidate, candidateIndex) => { + const { + encoded: compressed, + beforeTokens, + encodedTokens, + } = encodedResults[candidateIndex]; + const { functionResponse, rawContent: noncompressed } = candidate; + const functionName = + "name" in functionResponse ? functionResponse.name : "unknown"; + + // Counts come from the native pass on the "raw" baseline — the original + // serialization, matching Gemini's accounting. Present iff the item was + // encoded; a null compressed output means it could not be compressed. + if ( + compressed === null || + beforeTokens === null || + encodedTokens === null + ) { + logger.info( + { functionName }, + "convertToolResultsToToon: skipping - response cannot be compressed", + ); + return; + } + + const tokensBefore = beforeTokens; + const tokensAfter = encodedTokens; + + // Always count tokens + totalTokensBefore += tokensBefore; + + // Only apply compression if it actually saves tokens + if (tokensAfter < tokensBefore) { + totalTokensAfter += tokensAfter; + + logger.info( + { + functionName, + beforeLength: noncompressed.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + toonPreview: compressed.substring(0, 150), + provider: "gemini", + }, + "convertToolResultsToToon: compressed", + ); + logger.trace( + { + functionName, + before: noncompressed, + after: compressed, + provider: "gemini", + }, + "convertToolResultsToToon: before/after", + ); + + // Replace the part with the compressed response + partsFor(candidate.contentIndex)[candidate.partIndex] = { + functionResponse: { + ...functionResponse, + // Gemini expects response as Record, but we now have a TOON string + // We wrap it in a {"tool_result": ""} object to match the expected format + response: { tool_result: compressed } as Record, + }, }; + return; } - return content; + // Compression not applied - count non-compressed tokens to track total tokens anyway + totalTokensAfter += tokensBefore; + logger.info( + { + functionName, + tokensBefore, + tokensAfter, + provider: "gemini", + }, + "Skipping TOON compression - compressed output has more tokens", + ); }); logger.info( diff --git a/platform/backend/src/routes/proxy/adapters/minimax-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/minimax-toon-compression.test.ts new file mode 100644 index 00000000000..dd47d2d5b3a --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/minimax-toon-compression.test.ts @@ -0,0 +1,242 @@ +// Pins the MiniMax adapter's TOON compression cutover to the native addon: +// full transformed-messages exact equality (TOON content from the committed +// v3 golden corpus, everything else byte-equal). MiniMax-specific semantics +// pinned here: compression is applied UNCONDITIONALLY (no keep/reject — +// encoded tokens are always recorded, even when TOON is larger). Requires the +// built addon: mandatory in CI; locally it skips visibly — run +// `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + makeToolCall, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Minimax } from "@/types/llm-providers"; +import { minimaxAdapterFactory } from "./minimax"; + +type MinimaxRequest = Minimax.Types.ChatCompletionsRequest; + +// Corpus picks: a uniform array (TOON smaller), a wrapped [{type:"text",...}] +// payload (unwrapped, TOON smaller), malformed JSON (kept as-is), and a +// heterogeneous array whose TOON encoding is LARGER under the MiniMax +// tokenizer — MiniMax still applies it (unconditional apply). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const MALFORMED = corpusEntry("malformed-prose"); +const LARGER = corpusEntry("boundary-hetero-arr-1"); + +const countTokens = makeCountTokens("minimax"); +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ + provider: "minimax", + modelId: "MiniMax-M2", + }); + +describeNative("MiniMax adapter TOON compression (native addon)", () => { + test("transforms the full messages exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): MinimaxRequest => ({ + model: "MiniMax-M2", + temperature: 0.25, + messages: [ + { role: "system", content: "You are a filesystem assistant." }, + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + content: null, + tool_calls: [ + makeToolCall("call_uniform", "list_files"), + makeToolCall("call_wrapped", "read_wrapped"), + makeToolCall("call_malformed", "read_notes"), + makeToolCall("call_larger", "read_config"), + ], + }, + { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_malformed", + content: MALFORMED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_larger", + content: LARGER.rawContent, + }, + ], + }); + + const adapter = minimaxAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("MiniMax-M2"); + + // UNCONDITIONAL apply: every parseable result is replaced — including + // the one whose TOON encoding is larger. Only malformed content is kept. + const expectedMessages = makeRequest().messages; + expectedMessages[3] = { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.expected.encoded as string, + }; + expectedMessages[4] = { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.expected.encoded as string, + }; + expectedMessages[6] = { + role: "tool", + tool_call_id: "call_larger", + content: LARGER.expected.encoded as string, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedMessages); + + // Encoded tokens are always recorded, larger or not; malformed content + // is not counted at all. + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized) + + countTokens(LARGER.expected.normalized); + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string) + + countTokens(LARGER.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: Math.max(0, tokensBefore - tokensAfter), + wasEffective: tokensAfter < tokensBefore, + hadToolResults: true, + }); + }); + + test("applies native results to the right candidates when non-string tool messages are interleaved", async () => { + await upsertPricing(); + + // Non-string (array-content) tool messages are NOT candidates for the + // native batch: the native result index diverges from both the message + // index and the tool-message index — off-by-one positional application + // would compress the wrong message and fail the equality below. + const makeRequest = (): MinimaxRequest => ({ + model: "MiniMax-M2", + messages: [ + { role: "user", content: "Inspect the workspace." }, + { + role: "assistant", + content: null, + tool_calls: [ + makeToolCall("call_a", "read_notes"), + makeToolCall("call_block", "read_chunks"), + makeToolCall("call_b", "list_files"), + ], + }, + { + role: "tool", + tool_call_id: "call_a", + content: MALFORMED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_block", + content: [{ type: "text", text: "chunk one" }], + }, + { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.rawContent, + }, + ], + }); + + const adapter = minimaxAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("MiniMax-M2"); + + // Only B compresses; A is malformed and the array-content message is + // untouched. + const expectedMessages = makeRequest().messages; + expectedMessages[4] = { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.expected.encoded as string, + }; + expect(adapter.getProviderMessages()).toStrictEqual(expectedMessages); + + const tokensBefore = countTokens(UNIFORM.expected.normalized); + const tokensAfter = countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // The two cases the combined request cannot isolate. + test("a lone malformed result: zeroed stats (hadToolResults=false), message untouched", async () => { + await upsertPricing(); + + const adapter = minimaxAdapterFactory.createRequestAdapter({ + model: "MiniMax-M2", + messages: [ + { role: "user", content: "run the tool" }, + { role: "tool", tool_call_id: "call_1", content: MALFORMED.rawContent }, + ], + }); + const stats = await adapter.applyToonCompression("MiniMax-M2"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: MALFORMED.rawContent, + }); + }); + + test("unconditional apply: a lone larger encoding is still applied, wasEffective=false", async () => { + await upsertPricing(); + + const adapter = minimaxAdapterFactory.createRequestAdapter({ + model: "MiniMax-M2", + messages: [ + { role: "user", content: "run the tool" }, + { role: "tool", tool_call_id: "call_1", content: LARGER.rawContent }, + ], + }); + const stats = await adapter.applyToonCompression("MiniMax-M2"); + + const tokensBefore = countTokens(LARGER.expected.normalized); + const tokensAfter = countTokens(LARGER.expected.encoded as string); + expect(tokensAfter).toBeGreaterThan(tokensBefore); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: LARGER.expected.encoded as string, + }); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/minimax.ts b/platform/backend/src/routes/proxy/adapters/minimax.ts index 321584fca69..1c6ed55b4a9 100644 --- a/platform/backend/src/routes/proxy/adapters/minimax.ts +++ b/platform/backend/src/routes/proxy/adapters/minimax.ts @@ -1,5 +1,4 @@ import { ArchestraInternalErrorCode } from "@archestra/shared"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import config from "@/config"; import logger from "@/logging"; @@ -23,7 +22,7 @@ import type { import { extractCommonMessageText } from "@/types"; import type { Minimax } from "@/types/llm-providers"; import type { ToolCompressionStats } from "../utils/toon-conversion"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ============================================================================= // TYPE ALIASES @@ -900,12 +899,20 @@ async function convertToolResultsToToon( messages: MinimaxMessages, model: string, ): Promise<{ messages: MinimaxMessages; stats: ToolCompressionStats }> { - const tokenizer = getTokenizer("minimax"); let toolResultCount = 0; let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidate tool messages first so the native unwrap→parse→encode + // transform runs once per request (batched, off the JS thread); results are + // positional and reapplied by message index. + type MinimaxToolMessage = Extract; + const candidates: { + index: number; + message: MinimaxToolMessage; + content: string; + }[] = []; + messages.forEach((message, index) => { if (message.role === "tool") { logger.info( { @@ -917,48 +924,86 @@ async function convertToolResultsToToon( ); if (typeof message.content === "string") { - try { - const unwrapped = unwrapToolContent(message.content); - const parsed = JSON.parse(unwrapped); - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); - - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - totalTokensBefore += tokensBefore; - totalTokensAfter += tokensAfter; - toolResultCount++; - - logger.info( - { - toolCallId: message.tool_call_id, - tokensBefore, - tokensAfter, - tokensSaved: tokensBefore - tokensAfter, - provider: "minimax", - }, - "convertToolResultsToToon: tool result compressed", - ); - - return { - ...message, - content: compressed, - }; - } catch (err) { - logger.warn( - { err, toolCallId: message.tool_call_id }, - "Failed to compress tool result", - ); - return message; - } + candidates.push({ index, message, content: message.content }); } } - return message; + }); + + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.message.tool_call_id, + rawContent: candidate.content, + unwrap: true, + })), + "normalized", + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: candidates.length > 0, + skipReason: "addon_unavailable", + }, + }; + } + + const result = [...messages]; + candidates.forEach((candidate, candidateIndex) => { + const { + encoded: compressed, + beforeTokens, + encodedTokens, + } = encodedResults[candidateIndex]; + const { message } = candidate; + + // Counts come from the native pass (on the "normalized" baseline). Present + // iff the item was encoded; a null compressed output means it could not be + // compressed. + if ( + compressed === null || + beforeTokens === null || + encodedTokens === null + ) { + logger.warn( + { toolCallId: message.tool_call_id }, + "Failed to compress tool result", + ); + return; + } + + const tokensBefore = beforeTokens; + const tokensAfter = encodedTokens; + + totalTokensBefore += tokensBefore; + totalTokensAfter += tokensAfter; + toolResultCount++; + + logger.info( + { + toolCallId: message.tool_call_id, + tokensBefore, + tokensAfter, + tokensSaved: tokensBefore - tokensAfter, + provider: "minimax", + }, + "convertToolResultsToToon: tool result compressed", + ); + + // Unconditional apply: MiniMax always records the encoded output. + result[candidate.index] = { + ...message, + content: compressed, + }; }); logger.info( diff --git a/platform/backend/src/routes/proxy/adapters/openai-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/openai-toon-compression.test.ts new file mode 100644 index 00000000000..c10e349152d --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/openai-toon-compression.test.ts @@ -0,0 +1,253 @@ +// Pins the OpenAI adapter's TOON compression cutover to the native addon: +// full transformed-request exact equality (TOON content from the committed v3 +// golden corpus, everything else byte-equal) plus the accounting semantics +// the combined request cannot isolate (hadToolResults=false when nothing was +// parseable; a lone rejected payload counted in BOTH totals — the +// OpenAI-family rule). Requires the built addon: mandatory in CI; locally it +// skips visibly — run `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + makeToolCall, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { OpenAi } from "@/types"; +import { openaiAdapterFactory } from "./openai"; + +type OpenAiRequest = OpenAi.Types.ChatCompletionsRequest; + +// Corpus picks (see the corpus for content): a uniform array (compression +// wins), a wrapped [{type:"text",...}] payload (unwrapped, compression wins), +// malformed JSON (kept as-is), and a near-boundary object whose TOON encoding +// does not save tokens under the OpenAI tokenizer (rejected). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const MALFORMED = corpusEntry("malformed-prose"); +const NEAR_BOUNDARY = corpusEntry("boundary-obj-1"); + +const countTokens = makeCountTokens("openai"); +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ provider: "openai", modelId: "gpt-4o" }); + +describeNative("OpenAI adapter TOON compression (native addon)", () => { + test("transforms the full provider request exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): OpenAiRequest => ({ + model: "gpt-4o", + temperature: 0.25, + messages: [ + { role: "system", content: "You are a filesystem assistant." }, + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + content: null, + tool_calls: [ + makeToolCall("call_uniform", "list_files"), + makeToolCall("call_wrapped", "read_wrapped"), + makeToolCall("call_malformed", "read_notes"), + makeToolCall("call_boundary", "read_config"), + ], + }, + { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_malformed", + content: MALFORMED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }); + + const adapter = openaiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("gpt-4o"); + + const expectedRequest = makeRequest(); + // Compression wins for the uniform array and the wrapped payload; the + // malformed and near-boundary results keep their original content. + expectedRequest.messages[3] = { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.expected.encoded as string, + }; + expectedRequest.messages[4] = { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.expected.encoded as string, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + // Rejected payloads count their original tokens in BOTH totals; malformed + // content is not counted at all. + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized) + + boundaryTokens; + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string) + + boundaryTokens; + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right candidates when non-string tool messages are interleaved", async () => { + await upsertPricing(); + + // Non-string (array-content) tool messages are NOT candidates for the + // native batch. Interleaving them between string candidates means the + // native result index diverges from both the message index and the + // tool-message index — off-by-one positional application would compress + // the wrong message and fail the full-request equality below. + const makeRequest = (): OpenAiRequest => ({ + model: "gpt-4o", + messages: [ + { role: "user", content: "Inspect the workspace." }, + { + role: "assistant", + content: null, + tool_calls: [ + makeToolCall("call_a", "read_config"), + makeToolCall("call_block1", "read_chunks"), + makeToolCall("call_b", "list_files"), + makeToolCall("call_block2", "read_more_chunks"), + makeToolCall("call_c", "read_notes"), + ], + }, + { + role: "tool", + tool_call_id: "call_a", + content: NEAR_BOUNDARY.rawContent, + }, + { + role: "tool", + tool_call_id: "call_block1", + content: [{ type: "text", text: "chunk one" }], + }, + { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_block2", + content: [{ type: "text", text: "chunk two" }], + }, + { + role: "tool", + tool_call_id: "call_c", + content: MALFORMED.rawContent, + }, + ], + }); + + const adapter = openaiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("gpt-4o"); + + // Only B compresses; A is rejected (near-boundary), C is malformed, and + // the two array-content messages are untouched. + const expectedRequest = makeRequest(); + expectedRequest.messages[4] = { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.expected.encoded as string, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + boundaryTokens + countTokens(UNIFORM.expected.normalized); + const tokensAfter = + boundaryTokens + countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // The two accounting cases the combined request above cannot isolate. + test("a lone malformed result: zeroed stats (hadToolResults=false), message untouched", async () => { + await upsertPricing(); + + const adapter = openaiAdapterFactory.createRequestAdapter({ + model: "gpt-4o", + messages: [ + { role: "user", content: "run the tool" }, + { role: "tool", tool_call_id: "call_1", content: MALFORMED.rawContent }, + ], + }); + const stats = await adapter.applyToonCompression("gpt-4o"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: MALFORMED.rawContent, + }); + }); + + test("a lone rejected result: original counted in both totals, wasEffective=false", async () => { + await upsertPricing(); + + const adapter = openaiAdapterFactory.createRequestAdapter({ + model: "gpt-4o", + messages: [ + { role: "user", content: "run the tool" }, + { + role: "tool", + tool_call_id: "call_1", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }); + const stats = await adapter.applyToonCompression("gpt-4o"); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + expect(stats).toStrictEqual({ + tokensBefore: boundaryTokens, + tokensAfter: boundaryTokens, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: NEAR_BOUNDARY.rawContent, + }); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/openai.ts b/platform/backend/src/routes/proxy/adapters/openai.ts index 0ef2394cb93..2448a9929fb 100644 --- a/platform/backend/src/routes/proxy/adapters/openai.ts +++ b/platform/backend/src/routes/proxy/adapters/openai.ts @@ -3,7 +3,6 @@ import { ArchestraInternalErrorCode, type SupportedProvider, } from "@archestra/shared"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import OpenAIProvider from "openai"; import type { @@ -14,7 +13,6 @@ import config from "@/config"; import logger from "@/logging"; import { ModelModel } from "@/models"; import { metrics } from "@/observability"; -import { getTokenizer } from "@/tokenizers"; import type { ChunkProcessingResult, CommonMcpToolDefinition, @@ -44,7 +42,7 @@ import { isMcpImageBlock, } from "../utils/mcp-image"; import { stripBrowserToolsResults } from "../utils/summarize-tool-results"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ============================================================================= // TYPE ALIASES @@ -991,6 +989,10 @@ export class OpenAIStreamAdapter readonly provider: SupportedProvider; readonly state: StreamAccumulatorState; private currentToolCallIndices = new Map(); + // SSE encoding of each state.rawToolCallEvents entry, cached at accumulation + // time. getRawToolCallEvents runs once per tool-call chunk, so serializing + // there would re-encode the whole history per chunk (O(k^2) per stream). + private serializedToolCallEvents: string[] = []; // Set to the refusal text when the streamed response was replaced by a policy // refusal. formatEndSSE then finishes the turn as "stop" instead of replaying // the upstream "tool_calls" finish reason (a text-only turn ending in @@ -1104,7 +1106,10 @@ export class OpenAIStreamAdapter } } + // Serialize before pushing so a throwing stringify can't desync the two arrays. + const serializedChunk = `data: ${JSON.stringify(chunk)}\n\n`; this.state.rawToolCallEvents.push(chunk); + this.serializedToolCallEvents.push(serializedChunk); isToolCallChunk = true; } @@ -1152,9 +1157,7 @@ export class OpenAIStreamAdapter } getRawToolCallEvents(): string[] { - return this.state.rawToolCallEvents.map( - (event) => `data: ${JSON.stringify(event)}\n\n`, - ); + return this.serializedToolCallEvents; } formatCompleteTextSSE(text: string): string[] { @@ -1266,12 +1269,20 @@ export async function convertToolResultsToToon( messages: OpenAiMessages; stats: ToolCompressionStats; }> { - const tokenizer = getTokenizer(provider); let toolResultCount = 0; let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidate tool messages first so the native unwrap→parse→encode + // transform runs once per request (batched, off the JS thread); results are + // positional and reapplied by message index. + type OpenAiToolMessage = Extract; + const candidates: { + index: number; + message: OpenAiToolMessage; + content: string; + }[] = []; + messages.forEach((message, index) => { if (message.role === "tool") { logger.info( { @@ -1283,86 +1294,119 @@ export async function convertToolResultsToToon( ); if (typeof message.content === "string") { - try { - const unwrapped = unwrapToolContent(message.content); - const parsed = JSON.parse(unwrapped); - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); - - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - toolResultCount++; - - // Always count tokens - totalTokensBefore += tokensBefore; - - // Only apply compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensAfter += tokensAfter; - - logger.info( - { - toolCallId: message.tool_call_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - toonPreview: compressed.substring(0, 150), - provider, - }, - "convertToolResultsToToon: compressed", - ); - logger.trace( - { - toolCallId: message.tool_call_id, - before: noncompressed, - after: compressed, - provider, - supposedToBeJson: parsed, - }, - "convertToolResultsToToon: before/after", - ); - - return { - ...message, - content: compressed, - }; - } - - // Compression not applied - count non-compressed tokens to track total tokens anyway - totalTokensAfter += tokensBefore; - logger.info( - { - toolCallId: message.tool_call_id, - tokensBefore, - tokensAfter, - provider, - }, - "Skipping TOON compression - compressed output has more tokens", - ); - return message; - } catch { - logger.info( - { - toolCallId: message.tool_call_id, - contentPreview: - typeof message.content === "string" - ? message.content.substring(0, 100) - : "non-string", - }, - "Skipping TOON conversion - content is not JSON", - ); - return message; - } + candidates.push({ index, message, content: message.content }); } } + }); + + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.message.tool_call_id, + rawContent: candidate.content, + unwrap: true, + })), + "normalized", + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: candidates.length > 0, + skipReason: "addon_unavailable", + }, + }; + } + + const result = [...messages]; + candidates.forEach((candidate, candidateIndex) => { + const { + normalized, + encoded: compressed, + beforeTokens, + encodedTokens, + } = encodedResults[candidateIndex]; + const { message } = candidate; + + // Counts come from the native pass (on the "normalized" baseline). They are + // present iff the item was encoded; a null compressed output means the + // content was not JSON — keep the original either way. + if ( + compressed === null || + beforeTokens === null || + encodedTokens === null + ) { + logger.info( + { + toolCallId: message.tool_call_id, + contentPreview: candidate.content.substring(0, 100), + }, + "Skipping TOON conversion - content is not JSON", + ); + return; + } + + const tokensBefore = beforeTokens; + const tokensAfter = encodedTokens; + + toolResultCount++; + + // Always count tokens + totalTokensBefore += tokensBefore; - return message; + // Only apply compression if it actually saves tokens + if (tokensAfter < tokensBefore) { + totalTokensAfter += tokensAfter; + + logger.info( + { + toolCallId: message.tool_call_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + toonPreview: compressed.substring(0, 150), + provider, + }, + "convertToolResultsToToon: compressed", + ); + logger.trace( + { + toolCallId: message.tool_call_id, + before: normalized, + after: compressed, + provider, + }, + "convertToolResultsToToon: before/after", + ); + + result[candidate.index] = { + ...message, + content: compressed, + }; + return; + } + + // Compression not applied - count non-compressed tokens to track total tokens anyway + totalTokensAfter += tokensBefore; + logger.info( + { + toolCallId: message.tool_call_id, + tokensBefore, + tokensAfter, + provider, + }, + "Skipping TOON compression - compressed output has more tokens", + ); }); logger.info( diff --git a/platform/backend/src/routes/proxy/adapters/stream-replay.test.ts b/platform/backend/src/routes/proxy/adapters/stream-replay.test.ts new file mode 100644 index 00000000000..88c5622f9ba --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/stream-replay.test.ts @@ -0,0 +1,678 @@ +/** + * Exact full-event-sequence tests for the streaming tool-call replay path. + * + * The proxy handler replays tool-call events through + * `getRawToolCallEvents()` and dedups by ARRAY INDEX + * (llm-proxy-handler.ts:1213-1222, :1388-1401), so every stream adapter must + * return the full, index-stable, append-only event history on every call. + * + * Each test drives an adapter with a synthetic chunk stream through the exact + * write pattern of the handler and asserts the COMPLETE ordered sequence of + * SSE writes, byte for byte, for three scenarios: + * - immediately-streamed non-blocking tool call (replay after every chunk), + * - buffered-then-approved final flush, + * - blocked-then-refused discard. + * + * The three native paths (OpenAI, Anthropic, Bedrock) pin byte-identical + * pre-existing behavior. The model-router Anthropic->OpenAI wrapper pins the + * FIXED behavior: the tool name event AND every argument-delta event are + * delivered exactly once (previously the wrapper drained its buffer on each + * getter call, re-basing indices at 0, so the handler's index dedup dropped + * every event after the first). + */ +import { EventStreamCodec } from "@smithy/eventstream-codec"; +import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; +import { vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test } from "@/test"; +import type { LLMStreamAdapter, OpenAi } from "@/types"; +import { anthropicAdapterFactory } from "./anthropic"; +import { makeAnthropicOpenaiAdapterFactory } from "./anthropic-openai"; +import { bedrockAdapterFactory } from "./bedrock"; +import { openaiAdapterFactory } from "./openai"; + +const FAKE_NOW_MS = 1_767_225_600_000; // 2026-01-01T00:00:00Z +const FAKE_NOW_UNIX = Math.floor(FAKE_NOW_MS / 1000); +const REFUSAL_TEXT = "This tool call was blocked by policy."; + +const ARGUMENT_FRAGMENTS = ['{"query":"', "weather ", 'today"', "}"] as const; + +type Scenario = + | "streamed-non-blocking" + | "buffered-approved" + | "blocked-refused"; + +type SseWrite = string | Uint8Array; + +/** + * Replicates the handler's SSE write pattern exactly: + * - per-chunk: llm-proxy-handler.ts:1174-1230 (text sseData streams + * immediately; in the non-blocking scenario every tool-call chunk triggers + * a full replay deduped by array index), + * - post-stream: llm-proxy-handler.ts:1361-1405 (refusal events, or the + * final flush of un-streamed tool events, then formatEndSSE). + */ +function runHandlerWritePattern(params: { + adapter: LLMStreamAdapter; + chunks: TChunk[]; + scenario: Scenario; +}): SseWrite[] { + const { adapter, chunks, scenario } = params; + const writes: SseWrite[] = []; + const streamedEventIndices = new Set(); + + for (const chunk of chunks) { + const result = adapter.processChunk(chunk); + if (result.sseData) { + writes.push(result.sseData); + } else if (result.isToolCallChunk && scenario === "streamed-non-blocking") { + const allEvents = adapter.getRawToolCallEvents(); + for (let i = 0; i < allEvents.length; i++) { + if (!streamedEventIndices.has(i)) { + writes.push(allEvents[i]); + streamedEventIndices.add(i); + } + } + } + if (result.isFinal) { + break; + } + } + + if (scenario === "blocked-refused") { + for (const event of adapter.formatCompleteTextSSE(REFUSAL_TEXT)) { + writes.push(event); + } + } else if ( + adapter.state.toolCalls.length > 0 && + streamedEventIndices.size < adapter.getRawToolCallEvents().length + ) { + const allEvents = adapter.getRawToolCallEvents(); + for (let i = 0; i < allEvents.length; i++) { + if (!streamedEventIndices.has(i)) { + writes.push(allEvents[i]); + } + } + } + + writes.push(adapter.formatEndSSE()); + return writes; +} + +beforeEach(() => { + vi.useFakeTimers({ now: FAKE_NOW_MS }); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +// ============================================================================= +// Native OpenAI +// ============================================================================= + +const OPENAI_ID = "chatcmpl-replay-test"; +const OPENAI_MODEL = "gpt-test"; + +function openaiChunks(): OpenAi.Types.ChatCompletionChunk[] { + const base = { + id: OPENAI_ID, + object: "chat.completion.chunk" as const, + created: 1_700_000_000, + model: OPENAI_MODEL, + }; + return [ + { + ...base, + choices: [ + { + index: 0, + delta: { content: "Checking " }, + finish_reason: null, + logprobs: null, + }, + ], + }, + { + ...base, + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: "call_replay_0", + type: "function", + function: { name: "search_documents", arguments: "" }, + }, + ], + }, + finish_reason: null, + logprobs: null, + }, + ], + }, + ...ARGUMENT_FRAGMENTS.map((fragment) => ({ + ...base, + choices: [ + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: fragment } }], + }, + finish_reason: null, + logprobs: null, + }, + ], + })), + { + ...base, + choices: [ + { index: 0, delta: {}, finish_reason: "tool_calls", logprobs: null }, + ], + }, + { + ...base, + choices: [], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + }, + ] as OpenAi.Types.ChatCompletionChunk[]; +} + +// Expected serializations are computed from fresh chunk literals BEFORE the +// adapter sees anything, so a post-accumulation mutation inside the adapter +// would fail these tests. +const openaiExpected = { + textEvent: () => `data: ${JSON.stringify(openaiChunks()[0])}\n\n`, + toolEvents: () => + openaiChunks() + .slice(1, 6) + .map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`), + endEvent: (finishReason: "tool_calls" | "stop") => + `data: ${JSON.stringify({ + id: OPENAI_ID, + object: "chat.completion.chunk", + created: FAKE_NOW_UNIX, + model: OPENAI_MODEL, + choices: [{ index: 0, delta: {}, finish_reason: finishReason }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + })}\n\ndata: [DONE]\n\n`, + refusalEvent: () => + `data: ${JSON.stringify({ + id: OPENAI_ID, + object: "chat.completion.chunk", + created: FAKE_NOW_UNIX, + model: OPENAI_MODEL, + choices: [ + { + index: 0, + delta: { role: "assistant", content: REFUSAL_TEXT }, + finish_reason: null, + }, + ], + })}\n\n`, +}; + +describe("native OpenAI stream replay", () => { + test("streamed non-blocking tool call delivers every event once, in order", () => { + const writes = runHandlerWritePattern({ + adapter: openaiAdapterFactory.createStreamAdapter(), + chunks: openaiChunks(), + scenario: "streamed-non-blocking", + }); + + expect(writes).toEqual([ + openaiExpected.textEvent(), + ...openaiExpected.toolEvents(), + openaiExpected.endEvent("tool_calls"), + ]); + }); + + test("buffered-then-approved flushes the full event history at the end", () => { + const writes = runHandlerWritePattern({ + adapter: openaiAdapterFactory.createStreamAdapter(), + chunks: openaiChunks(), + scenario: "buffered-approved", + }); + + expect(writes).toEqual([ + openaiExpected.textEvent(), + ...openaiExpected.toolEvents(), + openaiExpected.endEvent("tool_calls"), + ]); + }); + + test("blocked-then-refused discards tool events and sends the refusal", () => { + const writes = runHandlerWritePattern({ + adapter: openaiAdapterFactory.createStreamAdapter(), + chunks: openaiChunks(), + scenario: "blocked-refused", + }); + + expect(writes).toEqual([ + openaiExpected.textEvent(), + openaiExpected.refusalEvent(), + openaiExpected.endEvent("stop"), + ]); + }); + + test("getRawToolCallEvents is non-destructive and index-stable across calls", () => { + const adapter = openaiAdapterFactory.createStreamAdapter(); + for (const chunk of openaiChunks()) { + adapter.processChunk(chunk); + } + const first = adapter.getRawToolCallEvents(); + const second = adapter.getRawToolCallEvents(); + expect([...first]).toEqual(openaiExpected.toolEvents()); + expect([...second]).toEqual([...first]); + }); +}); + +// ============================================================================= +// Native Anthropic +// ============================================================================= + +const ANTHROPIC_MESSAGE_ID = "msg-replay-test"; +const ANTHROPIC_MODEL = "claude-test"; + +type AnthropicStreamChunk = Parameters< + ReturnType["processChunk"] +>[0]; + +function anthropicChunkPayloads(): Record[] { + return [ + { + type: "message_start", + message: { + id: ANTHROPIC_MESSAGE_ID, + type: "message", + role: "assistant", + content: [], + model: ANTHROPIC_MODEL, + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 25, output_tokens: 1 }, + }, + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: "toolu_replay_0", + name: "search_documents", + input: {}, + }, + }, + ...ARGUMENT_FRAGMENTS.map((partial_json) => ({ + type: "content_block_delta", + index: 0, + delta: { type: "input_json_delta", partial_json }, + })), + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: "tool_use", stop_sequence: null }, + usage: { output_tokens: 9 }, + }, + { type: "message_stop" }, + ]; +} + +function anthropicChunks(): AnthropicStreamChunk[] { + return anthropicChunkPayloads() as unknown as AnthropicStreamChunk[]; +} + +function anthropicSse(payload: Record): string { + return `event: ${payload.type}\ndata: ${JSON.stringify(payload)}\n\n`; +} + +const anthropicExpected = { + messageStartEvent: () => anthropicSse(anthropicChunkPayloads()[0]), + toolEvents: () => anthropicChunkPayloads().slice(1, 7).map(anthropicSse), + endEvent: (stopReason: "tool_use" | "end_turn") => + anthropicSse({ + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 9 }, + }) + anthropicSse({ type: "message_stop" }), + refusalEvents: () => [ + anthropicSse({ + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }), + anthropicSse({ + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: REFUSAL_TEXT }, + }), + anthropicSse({ type: "content_block_stop", index: 0 }), + ], +}; + +describe("native Anthropic stream replay", () => { + test("streamed non-blocking tool call delivers every event once, in order", () => { + const writes = runHandlerWritePattern({ + adapter: anthropicAdapterFactory.createStreamAdapter(), + chunks: anthropicChunks(), + scenario: "streamed-non-blocking", + }); + + expect(writes).toEqual([ + anthropicExpected.messageStartEvent(), + ...anthropicExpected.toolEvents(), + anthropicExpected.endEvent("tool_use"), + ]); + }); + + test("buffered-then-approved flushes the full event history at the end", () => { + const writes = runHandlerWritePattern({ + adapter: anthropicAdapterFactory.createStreamAdapter(), + chunks: anthropicChunks(), + scenario: "buffered-approved", + }); + + expect(writes).toEqual([ + anthropicExpected.messageStartEvent(), + ...anthropicExpected.toolEvents(), + anthropicExpected.endEvent("tool_use"), + ]); + }); + + test("blocked-then-refused discards tool events and sends the refusal", () => { + const writes = runHandlerWritePattern({ + adapter: anthropicAdapterFactory.createStreamAdapter(), + chunks: anthropicChunks(), + scenario: "blocked-refused", + }); + + expect(writes).toEqual([ + anthropicExpected.messageStartEvent(), + ...anthropicExpected.refusalEvents(), + anthropicExpected.endEvent("end_turn"), + ]); + }); + + test("getRawToolCallEvents is non-destructive and index-stable across calls", () => { + const adapter = anthropicAdapterFactory.createStreamAdapter(); + for (const chunk of anthropicChunks()) { + adapter.processChunk(chunk); + } + const first = adapter.getRawToolCallEvents(); + const second = adapter.getRawToolCallEvents(); + expect([...first]).toEqual(anthropicExpected.toolEvents()); + expect([...second]).toEqual([...first]); + }); +}); + +// ============================================================================= +// Native Bedrock +// ============================================================================= + +type BedrockStreamChunk = Parameters< + ReturnType["processChunk"] +>[0]; + +const bedrockCodec = new EventStreamCodec(toUtf8, fromUtf8); + +// Independent re-implementation of the adapter's AWS event-stream encoding +// (bedrock.ts encodeEventStreamMessage + generatePadding) so expected bytes +// are pinned by the test, not derived from the code under test. +function encodeBedrockEvent( + eventType: string, + body: Record, +): Uint8Array { + const bodyWithoutPadding = JSON.stringify(body); + const paddingAlphabet = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const paddingNeeded = Math.max(0, 80 - bodyWithoutPadding.length - 10); + const padding = paddingAlphabet.slice( + 0, + Math.min(paddingNeeded, paddingAlphabet.length), + ); + return bedrockCodec.encode({ + headers: { + ":event-type": { type: "string", value: eventType }, + ":content-type": { type: "string", value: "application/json" }, + ":message-type": { type: "string", value: "event" }, + }, + body: fromUtf8(JSON.stringify({ ...body, p: padding })), + }); +} + +function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { + const out = new Uint8Array(a.length + b.length); + out.set(a, 0); + out.set(b, a.length); + return out; +} + +function bedrockChunks(): BedrockStreamChunk[] { + return [ + { messageStart: { role: "assistant" } }, + { + contentBlockStart: { + contentBlockIndex: 0, + start: { + toolUse: { toolUseId: "tooluse_replay_0", name: "search_documents" }, + }, + }, + }, + ...ARGUMENT_FRAGMENTS.map((input) => ({ + contentBlockDelta: { + contentBlockIndex: 0, + delta: { toolUse: { input } }, + }, + })), + { contentBlockStop: { contentBlockIndex: 0 } }, + { messageStop: { stopReason: "tool_use" } }, + { metadata: { usage: { inputTokens: 25, outputTokens: 9 } } }, + ] as BedrockStreamChunk[]; +} + +const bedrockExpected = { + messageStartEvent: () => + encodeBedrockEvent("messageStart", { role: "assistant" }), + toolEvents: () => [ + encodeBedrockEvent("contentBlockStart", { + contentBlockIndex: 0, + start: { + toolUse: { toolUseId: "tooluse_replay_0", name: "search_documents" }, + }, + }), + ...ARGUMENT_FRAGMENTS.map((input) => + encodeBedrockEvent("contentBlockDelta", { + contentBlockIndex: 0, + delta: { toolUse: { input } }, + }), + ), + encodeBedrockEvent("contentBlockStop", { contentBlockIndex: 0 }), + encodeBedrockEvent("messageStop", { stopReason: "tool_use" }), + encodeBedrockEvent("metadata", { + usage: { inputTokens: 25, outputTokens: 9 }, + }), + ], + refusalEvents: () => [ + encodeBedrockEvent("contentBlockStart", { + contentBlockIndex: 0, + start: { text: "" }, + }), + encodeBedrockEvent("contentBlockDelta", { + contentBlockIndex: 0, + delta: { text: REFUSAL_TEXT }, + }), + encodeBedrockEvent("contentBlockStop", { contentBlockIndex: 0 }), + ], + refusalEndEvent: () => + concatBytes( + encodeBedrockEvent("messageStop", { stopReason: "end_turn" }), + encodeBedrockEvent("metadata", { + usage: { inputTokens: 25, outputTokens: 9 }, + }), + ), +}; + +describe("native Bedrock stream replay", () => { + test("streamed non-blocking tool call delivers every event once, in order", () => { + const writes = runHandlerWritePattern({ + adapter: bedrockAdapterFactory.createStreamAdapter(), + chunks: bedrockChunks(), + scenario: "streamed-non-blocking", + }); + + expect(writes).toEqual([ + bedrockExpected.messageStartEvent(), + ...bedrockExpected.toolEvents(), + // Bedrock's formatEndSSE is empty on the non-refusal path: messageStop + // and metadata were already replayed with the tool events. + "", + ]); + }); + + test("buffered-then-approved flushes the full event history at the end", () => { + const writes = runHandlerWritePattern({ + adapter: bedrockAdapterFactory.createStreamAdapter(), + chunks: bedrockChunks(), + scenario: "buffered-approved", + }); + + expect(writes).toEqual([ + bedrockExpected.messageStartEvent(), + ...bedrockExpected.toolEvents(), + "", + ]); + }); + + test("blocked-then-refused discards tool events and sends the refusal", () => { + const writes = runHandlerWritePattern({ + adapter: bedrockAdapterFactory.createStreamAdapter(), + chunks: bedrockChunks(), + scenario: "blocked-refused", + }); + + expect(writes).toEqual([ + bedrockExpected.messageStartEvent(), + ...bedrockExpected.refusalEvents(), + bedrockExpected.refusalEndEvent(), + ]); + }); + + test("getRawToolCallEvents is non-destructive and index-stable across calls", () => { + const adapter = bedrockAdapterFactory.createStreamAdapter(); + for (const chunk of bedrockChunks()) { + adapter.processChunk(chunk); + } + const first = adapter.getRawToolCallEvents(); + const second = adapter.getRawToolCallEvents(); + expect([...first]).toEqual(bedrockExpected.toolEvents()); + expect([...second]).toEqual([...first]); + }); +}); + +// ============================================================================= +// Model-router Anthropic -> OpenAI wrapper +// ============================================================================= + +const WRAPPER_CTX = { + chatcmplId: "chatcmpl-router-test", + createdUnix: 1_700_000_123, + requestedModel: "anthropic:claude-test", +}; + +function wrapperAdapter() { + return makeAnthropicOpenaiAdapterFactory(WRAPPER_CTX).createStreamAdapter(); +} + +function wrapperChunkSse( + delta: Record, + finishReason: string | null = null, +): string { + return `data: ${JSON.stringify({ + id: WRAPPER_CTX.chatcmplId, + object: "chat.completion.chunk", + created: WRAPPER_CTX.createdUnix, + model: WRAPPER_CTX.requestedModel, + choices: [{ index: 0, delta, finish_reason: finishReason, logprobs: null }], + })}\n\n`; +} + +const wrapperExpected = { + roleEvent: () => wrapperChunkSse({ role: "assistant" }), + toolEvents: () => [ + wrapperChunkSse({ + tool_calls: [ + { + index: 0, + id: "toolu_replay_0", + type: "function", + function: { name: "search_documents", arguments: "" }, + }, + ], + }), + ...ARGUMENT_FRAGMENTS.map((fragment) => + wrapperChunkSse({ + tool_calls: [{ index: 0, function: { arguments: fragment } }], + }), + ), + ], + endEvent: (finishReason: "tool_calls" | "stop") => + `${wrapperChunkSse({}, finishReason)}data: [DONE]\n\n`, + refusalEvent: () => + wrapperChunkSse({ role: "assistant", content: REFUSAL_TEXT }), +}; + +describe("model-router Anthropic->OpenAI wrapper stream replay (fixed behavior)", () => { + test("streamed non-blocking tool call delivers the name event and every argument delta once", () => { + const writes = runHandlerWritePattern({ + adapter: wrapperAdapter(), + chunks: anthropicChunks(), + scenario: "streamed-non-blocking", + }); + + expect(writes).toEqual([ + wrapperExpected.roleEvent(), + ...wrapperExpected.toolEvents(), + wrapperExpected.endEvent("tool_calls"), + ]); + }); + + test("buffered-then-approved flushes the full event history at the end", () => { + const writes = runHandlerWritePattern({ + adapter: wrapperAdapter(), + chunks: anthropicChunks(), + scenario: "buffered-approved", + }); + + expect(writes).toEqual([ + wrapperExpected.roleEvent(), + ...wrapperExpected.toolEvents(), + wrapperExpected.endEvent("tool_calls"), + ]); + }); + + test("blocked-then-refused discards tool events and sends the refusal", () => { + const writes = runHandlerWritePattern({ + adapter: wrapperAdapter(), + chunks: anthropicChunks(), + scenario: "blocked-refused", + }); + + expect(writes).toEqual([ + wrapperExpected.roleEvent(), + wrapperExpected.refusalEvent(), + wrapperExpected.endEvent("stop"), + ]); + }); + + test("getRawToolCallEvents is non-destructive and index-stable across calls", () => { + const adapter = wrapperAdapter(); + for (const chunk of anthropicChunks()) { + adapter.processChunk(chunk); + } + const first = adapter.getRawToolCallEvents(); + const second = adapter.getRawToolCallEvents(); + expect([...first]).toEqual(wrapperExpected.toolEvents()); + expect([...second]).toEqual([...first]); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/toon-compression-addon-unavailable.test.ts b/platform/backend/src/routes/proxy/adapters/toon-compression-addon-unavailable.test.ts new file mode 100644 index 00000000000..a3642004685 --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/toon-compression-addon-unavailable.test.ts @@ -0,0 +1,250 @@ +// Pins the addon-unavailable contract at the adapter level for every adapter +// cut over to the native TOON kernel: when the helper fails open (resolves to +// null), the adapter leaves the messages untouched — the same object, not +// just structurally equal — and reports the explicit `addon_unavailable` skip +// reason with zeroed totals, never stats fabricated from a transform that did +// not run. Each case also pins the batching contract: the helper is called +// EXACTLY once per request with the full candidate batch. (The OpenAI +// adapter's handler-level counterpart lives in +// routes/toon-addon-unavailable.test.ts.) + +import { vi } from "vitest"; +import { beforeEach, describe, expect, test } from "@/test"; +import type { ToolCompressionStats } from "@/types"; +import { toonEncodeToolResults } from "../utils/toon-native"; +import { anthropicAdapterFactory } from "./anthropic"; +import { bedrockAdapterFactory } from "./bedrock"; +import { cohereAdapterFactory } from "./cohere"; +import { geminiAdapterFactory } from "./gemini"; +import { minimaxAdapterFactory } from "./minimax"; +import { zhipuaiAdapterFactory } from "./zhipuai"; + +// The native addon is unavailable: the helper fails open by resolving to null. +vi.mock("@/routes/proxy/utils/toon-native", () => ({ + toonEncodeToolResults: vi.fn(), + initToonNative: vi.fn(), +})); + +const TOOL_RESULT_JSON_A = JSON.stringify({ + files: [{ name: "README.md" }, { name: "src" }], +}); +const TOOL_RESULT_JSON_B = JSON.stringify({ config: { debug: true } }); + +const EXPECTED_STATS: ToolCompressionStats = { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + skipReason: "addon_unavailable", +}; + +function expectSingleBatchCall( + expectedItems: { id: string; rawContent: string; unwrap: boolean }[], + beforeSource?: "raw" | "normalized", +) { + expect(vi.mocked(toonEncodeToolResults)).toHaveBeenCalledTimes(1); + // Anthropic and Bedrock keep their own tokenizer and call with one argument; + // the tiktoken-family adapters request native counting with a before-source. + if (beforeSource === undefined) { + expect(vi.mocked(toonEncodeToolResults)).toHaveBeenCalledWith( + expectedItems, + ); + } else { + expect(vi.mocked(toonEncodeToolResults)).toHaveBeenCalledWith( + expectedItems, + beforeSource, + ); + } +} + +describe("adapters with the TOON addon unavailable", () => { + beforeEach(() => { + vi.mocked(toonEncodeToolResults).mockReset().mockResolvedValue(null); + }); + + test("anthropic: one batched call, same messages object, addon_unavailable stats", async () => { + const messages = [ + { + role: "user" as const, + content: [ + { + type: "tool_result" as const, + tool_use_id: "toolu_1", + content: TOOL_RESULT_JSON_A, + }, + { + type: "tool_result" as const, + tool_use_id: "toolu_2", + content: [{ type: "text" as const, text: TOOL_RESULT_JSON_B }], + }, + ], + }, + ]; + const adapter = anthropicAdapterFactory.createRequestAdapter({ + model: "claude-sonnet-4-5", + max_tokens: 1024, + messages, + }); + const stats = await adapter.applyToonCompression("claude-sonnet-4-5"); + + expectSingleBatchCall([ + { id: "toolu_1", rawContent: TOOL_RESULT_JSON_A, unwrap: true }, + { id: "toolu_2", rawContent: TOOL_RESULT_JSON_B, unwrap: true }, + ]); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(messages); + }); + + test("gemini: one batched call, same contents object, addon_unavailable stats", async () => { + const responseA = JSON.parse(TOOL_RESULT_JSON_A); + const responseB = JSON.parse(TOOL_RESULT_JSON_B); + const contents = [ + { + role: "user", + parts: [ + { functionResponse: { name: "list_files", response: responseA } }, + { functionResponse: { name: "read_config", response: responseB } }, + ], + }, + ]; + const adapter = geminiAdapterFactory.createRequestAdapter({ contents }); + const stats = await adapter.applyToonCompression("gemini-2.0-flash"); + + expectSingleBatchCall( + [ + { id: "list_files", rawContent: TOOL_RESULT_JSON_A, unwrap: true }, + { id: "read_config", rawContent: TOOL_RESULT_JSON_B, unwrap: true }, + ], + "raw", + ); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(contents); + }); + + test("bedrock: one batched call, same messages object, addon_unavailable stats", async () => { + const messages = [ + { + role: "user" as const, + content: [ + { + toolResult: { + toolUseId: "tooluse_1", + content: [{ text: TOOL_RESULT_JSON_A }], + }, + }, + { + toolResult: { + toolUseId: "tooluse_2", + content: [{ json: JSON.parse(TOOL_RESULT_JSON_B) }], + }, + }, + ], + }, + ]; + const adapter = bedrockAdapterFactory.createRequestAdapter({ + modelId: "anthropic.claude-sonnet-4-5-20250929-v1:0", + messages, + }); + const stats = await adapter.applyToonCompression( + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ); + + expectSingleBatchCall([ + { id: "tooluse_1", rawContent: TOOL_RESULT_JSON_A, unwrap: false }, + { id: "tooluse_2", rawContent: TOOL_RESULT_JSON_B, unwrap: false }, + ]); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(messages); + }); + + test("zhipuai: one batched call, same messages object, addon_unavailable stats", async () => { + const messages = [ + { + role: "tool" as const, + tool_call_id: "call_1", + content: TOOL_RESULT_JSON_A, + }, + { + role: "tool" as const, + tool_call_id: "call_2", + content: TOOL_RESULT_JSON_B, + }, + ]; + const adapter = zhipuaiAdapterFactory.createRequestAdapter({ + model: "glm-4.6", + messages, + }); + const stats = await adapter.applyToonCompression("glm-4.6"); + + expectSingleBatchCall( + [ + { id: "call_1", rawContent: TOOL_RESULT_JSON_A, unwrap: true }, + { id: "call_2", rawContent: TOOL_RESULT_JSON_B, unwrap: true }, + ], + "normalized", + ); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(messages); + }); + + test("minimax: one batched call, same messages object, addon_unavailable stats", async () => { + const messages = [ + { + role: "tool" as const, + tool_call_id: "call_1", + content: TOOL_RESULT_JSON_A, + }, + { + role: "tool" as const, + tool_call_id: "call_2", + content: TOOL_RESULT_JSON_B, + }, + ]; + const adapter = minimaxAdapterFactory.createRequestAdapter({ + model: "MiniMax-M2", + messages, + }); + const stats = await adapter.applyToonCompression("MiniMax-M2"); + + expectSingleBatchCall( + [ + { id: "call_1", rawContent: TOOL_RESULT_JSON_A, unwrap: true }, + { id: "call_2", rawContent: TOOL_RESULT_JSON_B, unwrap: true }, + ], + "normalized", + ); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(messages); + }); + + test("cohere: one batched call, same messages object, addon_unavailable stats", async () => { + const messages = [ + { + role: "tool" as const, + tool_call_id: "call_1", + content: TOOL_RESULT_JSON_A, + }, + { + role: "tool" as const, + tool_call_id: "call_2", + content: TOOL_RESULT_JSON_B, + }, + ]; + const adapter = cohereAdapterFactory.createRequestAdapter({ + model: "command-r-plus", + messages, + }); + const stats = await adapter.applyToonCompression("command-r-plus"); + + expectSingleBatchCall( + [ + { id: "call_1", rawContent: TOOL_RESULT_JSON_A, unwrap: true }, + { id: "call_2", rawContent: TOOL_RESULT_JSON_B, unwrap: true }, + ], + "normalized", + ); + expect(stats).toStrictEqual(EXPECTED_STATS); + expect(adapter.getProviderMessages()).toBe(messages); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/zhipuai-toon-compression.test.ts b/platform/backend/src/routes/proxy/adapters/zhipuai-toon-compression.test.ts new file mode 100644 index 00000000000..700eae7be55 --- /dev/null +++ b/platform/backend/src/routes/proxy/adapters/zhipuai-toon-compression.test.ts @@ -0,0 +1,241 @@ +// Pins the ZhipuAI adapter's TOON compression cutover to the native addon: +// full transformed-request exact equality (TOON content from the committed v3 +// golden corpus, everything else byte-equal) plus the accounting semantics +// the combined request cannot isolate (hadToolResults=false when nothing was +// parseable; a lone rejected payload counted in BOTH totals — the +// OpenAI-family rule). Requires the built addon: mandatory in CI; locally it +// skips visibly — run `pnpm test:native` from platform/backend. + +import { expect, test } from "@/test"; +import { + corpusEntry, + describeNative, + makeCountTokens, + makeToolCall, + upsertOneDollarPerTokenPricing, +} from "@/test/toon-golden"; +import type { Zhipuai } from "@/types"; +import { zhipuaiAdapterFactory } from "./zhipuai"; + +type ZhipuaiRequest = Zhipuai.Types.ChatCompletionsRequest; + +// Corpus picks: a uniform array (compression wins), a wrapped +// [{type:"text",...}] payload (unwrapped, compression wins), malformed JSON +// (kept as-is), and a near-boundary object whose TOON encoding does not save +// tokens under the ZhipuAI tokenizer (rejected). +const UNIFORM = corpusEntry("provider-matrix-tool-result"); +const WRAPPED = corpusEntry("wrapped-single-text"); +const MALFORMED = corpusEntry("malformed-prose"); +const NEAR_BOUNDARY = corpusEntry("boundary-obj-1"); + +const countTokens = makeCountTokens("zhipuai"); +const upsertPricing = () => + upsertOneDollarPerTokenPricing({ provider: "zhipuai", modelId: "glm-4.6" }); + +describeNative("ZhipuAI adapter TOON compression (native addon)", () => { + test("transforms the full provider request exactly (goldens for TOON, byte-equal elsewhere)", async () => { + await upsertPricing(); + + const makeRequest = (): ZhipuaiRequest => ({ + model: "glm-4.6", + temperature: 0.25, + messages: [ + { role: "system", content: "You are a filesystem assistant." }, + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + tool_calls: [ + makeToolCall("call_uniform", "list_files"), + makeToolCall("call_wrapped", "read_wrapped"), + makeToolCall("call_malformed", "read_notes"), + makeToolCall("call_boundary", "read_config"), + ], + }, + { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_malformed", + content: MALFORMED.rawContent, + }, + { + role: "tool", + tool_call_id: "call_boundary", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }); + + const adapter = zhipuaiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("glm-4.6"); + + const expectedRequest = makeRequest(); + // Compression wins for the uniform array and the wrapped payload; the + // malformed and near-boundary results keep their original content. + expectedRequest.messages[3] = { + role: "tool", + tool_call_id: "call_uniform", + content: UNIFORM.expected.encoded as string, + }; + expectedRequest.messages[4] = { + role: "tool", + tool_call_id: "call_wrapped", + content: WRAPPED.expected.encoded as string, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + // Rejected payloads count their original tokens in BOTH totals; malformed + // content is not counted at all. + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + countTokens(UNIFORM.expected.normalized) + + countTokens(WRAPPED.expected.normalized) + + boundaryTokens; + const tokensAfter = + countTokens(UNIFORM.expected.encoded as string) + + countTokens(WRAPPED.expected.encoded as string) + + boundaryTokens; + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + test("applies native results to the right candidates when non-candidate messages are interleaved", async () => { + await upsertPricing(); + + // ZhipuAI tool messages are string-only, so non-candidates here are + // assistant/user messages between tool messages: the native result index + // diverges from the message index — off-by-one positional application + // would compress the wrong message and fail the equality below. + const makeRequest = (): ZhipuaiRequest => ({ + model: "glm-4.6", + messages: [ + { role: "user", content: "Inspect the workspace." }, + { + role: "assistant", + tool_calls: [makeToolCall("call_a", "read_config")], + }, + { + role: "tool", + tool_call_id: "call_a", + content: NEAR_BOUNDARY.rawContent, + }, + { role: "assistant", content: "Let me look further." }, + { + role: "assistant", + tool_calls: [ + makeToolCall("call_b", "list_files"), + makeToolCall("call_c", "read_notes"), + ], + }, + { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.rawContent, + }, + { + role: "tool", + tool_call_id: "call_c", + content: MALFORMED.rawContent, + }, + ], + }); + + const adapter = zhipuaiAdapterFactory.createRequestAdapter(makeRequest()); + const stats = await adapter.applyToonCompression("glm-4.6"); + + // Only B compresses; A is rejected (near-boundary), C is malformed. + const expectedRequest = makeRequest(); + expectedRequest.messages[5] = { + role: "tool", + tool_call_id: "call_b", + content: UNIFORM.expected.encoded as string, + }; + expect(adapter.toProviderRequest()).toStrictEqual(expectedRequest); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + const tokensBefore = + boundaryTokens + countTokens(UNIFORM.expected.normalized); + const tokensAfter = + boundaryTokens + countTokens(UNIFORM.expected.encoded as string); + expect(stats).toStrictEqual({ + tokensBefore, + tokensAfter, + costSavings: tokensBefore - tokensAfter, + wasEffective: true, + hadToolResults: true, + }); + }); + + // The two accounting cases the combined request above cannot isolate. + test("a lone malformed result: zeroed stats (hadToolResults=false), message untouched", async () => { + await upsertPricing(); + + const adapter = zhipuaiAdapterFactory.createRequestAdapter({ + model: "glm-4.6", + messages: [ + { role: "user", content: "run the tool" }, + { role: "tool", tool_call_id: "call_1", content: MALFORMED.rawContent }, + ], + }); + const stats = await adapter.applyToonCompression("glm-4.6"); + + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: MALFORMED.rawContent, + }); + }); + + test("a lone rejected result: original counted in both totals, wasEffective=false", async () => { + await upsertPricing(); + + const adapter = zhipuaiAdapterFactory.createRequestAdapter({ + model: "glm-4.6", + messages: [ + { role: "user", content: "run the tool" }, + { + role: "tool", + tool_call_id: "call_1", + content: NEAR_BOUNDARY.rawContent, + }, + ], + }); + const stats = await adapter.applyToonCompression("glm-4.6"); + + const boundaryTokens = countTokens(NEAR_BOUNDARY.expected.normalized); + expect(stats).toStrictEqual({ + tokensBefore: boundaryTokens, + tokensAfter: boundaryTokens, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + }); + const [, toolMessage] = adapter.getProviderMessages(); + expect(toolMessage).toStrictEqual({ + role: "tool", + tool_call_id: "call_1", + content: NEAR_BOUNDARY.rawContent, + }); + }); +}); diff --git a/platform/backend/src/routes/proxy/adapters/zhipuai.ts b/platform/backend/src/routes/proxy/adapters/zhipuai.ts index c930c1640f5..afece76680f 100644 --- a/platform/backend/src/routes/proxy/adapters/zhipuai.ts +++ b/platform/backend/src/routes/proxy/adapters/zhipuai.ts @@ -2,13 +2,11 @@ import { ArchestraInternalErrorCode, ZhipuaiErrorTypes, } from "@archestra/shared"; -import { encode as toonEncode } from "@toon-format/toon"; import { get } from "lodash-es"; import config from "@/config"; import logger from "@/logging"; import { ModelModel } from "@/models"; import { metrics } from "@/observability"; -import { getTokenizer } from "@/tokenizers"; import type { ChunkProcessingResult, CommonMcpToolDefinition, @@ -26,7 +24,7 @@ import type { Zhipuai, } from "@/types"; import { extractCommonMessageText } from "@/types"; -import { unwrapToolContent } from "../utils/unwrap-tool-content"; +import { toonEncodeToolResults } from "../utils/toon-native"; // ============================================================================= // TYPE ALIASES @@ -826,12 +824,20 @@ async function convertToolResultsToToon( messages: ZhipuaiMessages; stats: ToolCompressionStats; }> { - const tokenizer = getTokenizer("zhipuai"); let toolResultCount = 0; let totalTokensBefore = 0; let totalTokensAfter = 0; - const result = messages.map((message) => { + // Collect candidate tool messages first so the native unwrap→parse→encode + // transform runs once per request (batched, off the JS thread); results are + // positional and reapplied by message index. + type ZhipuaiToolMessage = Extract; + const candidates: { + index: number; + message: ZhipuaiToolMessage; + content: string; + }[] = []; + messages.forEach((message, index) => { if (message.role === "tool") { logger.info( { @@ -843,85 +849,118 @@ async function convertToolResultsToToon( ); if (typeof message.content === "string") { - try { - const unwrapped = unwrapToolContent(message.content); - const parsed = JSON.parse(unwrapped); - const noncompressed = unwrapped; - const compressed = toonEncode(parsed); - - const tokensBefore = tokenizer.countTokens([ - { role: "user", content: noncompressed }, - ]); - const tokensAfter = tokenizer.countTokens([ - { role: "user", content: compressed }, - ]); - - toolResultCount++; - - // Always count tokens before - totalTokensBefore += tokensBefore; - - // Only apply compression if it actually saves tokens - if (tokensAfter < tokensBefore) { - totalTokensAfter += tokensAfter; - - logger.info( - { - toolCallId: message.tool_call_id, - beforeLength: noncompressed.length, - afterLength: compressed.length, - tokensBefore, - tokensAfter, - toonPreview: compressed.substring(0, 150), - provider: "zhipuai", - }, - "convertToolResultsToToon: compressed", - ); - logger.trace( - { - toolCallId: message.tool_call_id, - before: noncompressed, - after: compressed, - provider: "zhipuai", - supposedToBeJson: parsed, - }, - "convertToolResultsToToon: before/after", - ); + candidates.push({ index, message, content: message.content }); + } + } + }); - return { - ...message, - content: compressed, - }; - } + const encodedResults = + candidates.length > 0 + ? await toonEncodeToolResults( + candidates.map((candidate) => ({ + id: candidate.message.tool_call_id, + rawContent: candidate.content, + unwrap: true, + })), + "normalized", + ) + : []; + + if (encodedResults === null) { + // Native addon unavailable: fail open — keep every message uncompressed + // and surface the explicit skip reason instead of fabricating stats. + return { + messages, + stats: { + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: candidates.length > 0, + skipReason: "addon_unavailable", + }, + }; + } - // Compression not applied - count non-compressed tokens to track total tokens anyway - totalTokensAfter += tokensBefore; - logger.info( - { - toolCallId: message.tool_call_id, - tokensBefore, - tokensAfter, - provider: "zhipuai", - }, - "Skipping TOON compression - compressed output has more tokens", - ); - } catch { - logger.info( - { - toolCallId: message.tool_call_id, - contentPreview: - typeof message.content === "string" - ? message.content.substring(0, 100) - : "non-string", - }, - "Skipping TOON conversion - content is not JSON", - ); - return message; - } - } + const result = [...messages]; + candidates.forEach((candidate, candidateIndex) => { + const { + normalized, + encoded: compressed, + beforeTokens, + encodedTokens, + } = encodedResults[candidateIndex]; + const { message } = candidate; + + // Counts come from the native pass (on the "normalized" baseline). Present + // iff the item was encoded; a null compressed output means non-JSON content. + if ( + compressed === null || + beforeTokens === null || + encodedTokens === null + ) { + logger.info( + { + toolCallId: message.tool_call_id, + contentPreview: candidate.content.substring(0, 100), + }, + "Skipping TOON conversion - content is not JSON", + ); + return; + } + + const tokensBefore = beforeTokens; + const tokensAfter = encodedTokens; + + toolResultCount++; + + // Always count tokens before + totalTokensBefore += tokensBefore; + + // Only apply compression if it actually saves tokens + if (tokensAfter < tokensBefore) { + totalTokensAfter += tokensAfter; + + logger.info( + { + toolCallId: message.tool_call_id, + beforeLength: normalized.length, + afterLength: compressed.length, + tokensBefore, + tokensAfter, + toonPreview: compressed.substring(0, 150), + provider: "zhipuai", + }, + "convertToolResultsToToon: compressed", + ); + logger.trace( + { + toolCallId: message.tool_call_id, + before: normalized, + after: compressed, + provider: "zhipuai", + }, + "convertToolResultsToToon: before/after", + ); + + result[candidate.index] = { + ...message, + content: compressed, + }; + return; } - return message; + // Compression not applied - count non-compressed tokens to track total tokens anyway + totalTokensAfter += tokensBefore; + logger.info( + { + toolCallId: message.tool_call_id, + tokensBefore, + tokensAfter, + provider: "zhipuai", + }, + "Skipping TOON compression - compressed output has more tokens", + ); }); logger.info( diff --git a/platform/backend/src/routes/proxy/llm-proxy-handler.ts b/platform/backend/src/routes/proxy/llm-proxy-handler.ts index 4b83da7829c..cbde92f47ea 100644 --- a/platform/backend/src/routes/proxy/llm-proxy-handler.ts +++ b/platform/backend/src/routes/proxy/llm-proxy-handler.ts @@ -839,7 +839,11 @@ export async function handleLLMProxy< if (shouldApplyToonCompression) { toonStats = await requestAdapter.applyToonCompression(actualModel); - if (!toonStats.hadToolResults) { + if (toonStats.skipReason) { + // Infrastructure failure (native addon unavailable) — never derive + // not_effective/no_tool_results from stats that were never computed. + toonSkipReason = toonStats.skipReason; + } else if (!toonStats.hadToolResults) { toonSkipReason = "no_tool_results"; } else if (!toonStats.wasEffective) { toonSkipReason = "not_effective"; diff --git a/platform/backend/src/routes/proxy/routes/model-router.test.ts b/platform/backend/src/routes/proxy/routes/model-router.test.ts index d930a0fbc3b..f6b97dbfb1e 100644 --- a/platform/backend/src/routes/proxy/routes/model-router.test.ts +++ b/platform/backend/src/routes/proxy/routes/model-router.test.ts @@ -1947,6 +1947,125 @@ describe("model router proxy routes", () => { expect(response.body).not.toContain("event: message_start"); }); + test("streams Anthropic model router tool calls with the name event and every argument delta", async ({ + makeAgent, + makeOrganization, + makeSecret, + makeLlmProviderApiKey, + }) => { + anthropicStubOptions.includeToolUse = true; + const app = createFastifyApp(); + await app.register(modelRouterProxyRoutes); + await upsertModel({ + provider: "anthropic", + modelId: "claude-opus-4-6-20250918", + }); + const organization = await makeOrganization(); + const { value } = await createModelRouterVirtualKey({ + organizationId: organization.id, + provider: "anthropic", + makeSecret, + makeLlmProviderApiKey, + apiKeyValue: "test-anthropic-key", + }); + const agent = await makeAgent({ + organizationId: organization.id, + name: "Model Router Tool Streaming Agent", + agentType: "llm_proxy", + }); + + const response = await app.inject({ + method: "POST", + url: `/v1/model-router/${agent.id}/chat/completions`, + headers: { + "content-type": "application/json", + authorization: `Bearer ${value}`, + "user-agent": "test-client", + }, + payload: { + model: "anthropic:claude-opus-4-6-20250918", + messages: [{ role: "user", content: "What is the weather?" }], + stream: true, + }, + }); + + expect(response.statusCode).toBe(200); + + const dataLines = response.body + .split("\n\n") + .map((event) => event.trim()) + .filter((event) => event.startsWith("data: ")) + .map((event) => event.slice("data: ".length)); + expect(dataLines.at(-1)).toBe("[DONE]"); + + // The complete ordered choice sequence: the tool-call name event and every + // argument delta must each arrive exactly once. The wrapper adapter used + // to drain its event buffer per getRawToolCallEvents call, which made the + // handler's index dedup drop every argument delta after the name event. + const choices = dataLines.slice(0, -1).map((line) => { + const chunk = JSON.parse(line) as { + object: string; + choices: Array<{ delta: unknown; finish_reason: string | null }>; + }; + expect(chunk.object).toBe("chat.completion.chunk"); + return chunk.choices[0]; + }); + expect(choices).toEqual([ + { + index: 0, + delta: { role: "assistant" }, + finish_reason: null, + logprobs: null, + }, + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: "toolu_test_weather", + type: "function", + function: { name: "get_weather", arguments: "" }, + }, + ], + }, + finish_reason: null, + logprobs: null, + }, + { + index: 0, + delta: { + tool_calls: [{ index: 0, function: { arguments: '{"location":"' } }], + }, + finish_reason: null, + logprobs: null, + }, + { + index: 0, + delta: { + tool_calls: [ + { index: 0, function: { arguments: 'San Francisco",' } }, + ], + }, + finish_reason: null, + logprobs: null, + }, + { + index: 0, + delta: { + tool_calls: [ + { index: 0, function: { arguments: '"unit":"fahrenheit"}' } }, + ], + }, + finish_reason: null, + logprobs: null, + }, + // The stub's message_delta reports stop_reason "end_turn", which the + // wrapper maps to "stop". + { index: 0, delta: {}, finish_reason: "stop", logprobs: null }, + ]); + }); + test("lists only models for providers mapped on the virtual key", async ({ makeAgent, makeOrganization, diff --git a/platform/backend/src/routes/proxy/routes/toon-addon-unavailable.test.ts b/platform/backend/src/routes/proxy/routes/toon-addon-unavailable.test.ts new file mode 100644 index 00000000000..44ae8f77886 --- /dev/null +++ b/platform/backend/src/routes/proxy/routes/toon-addon-unavailable.test.ts @@ -0,0 +1,147 @@ +// Pins the addon-unavailable contract at the handler level: when TOON +// compression is enabled but the native proxy-transform addon cannot be +// loaded, the proxy fails open (request still succeeds, uncompressed) and the +// persisted interaction records toonSkipReason = "addon_unavailable" — never +// not_effective/no_tool_results fabricated from stats that were never computed. + +import Fastify, { type FastifyInstance } from "fastify"; +import { + serializerCompiler, + validatorCompiler, + type ZodTypeProvider, +} from "fastify-type-provider-zod"; +import { vi } from "vitest"; +import { InteractionModel } from "@/models"; +import { afterEach, beforeEach, describe, expect, test } from "@/test"; +import { openaiAdapterFactory } from "../adapters/openai"; +import * as proxyUtils from "../utils"; +import { toonEncodeToolResults } from "../utils/toon-native"; +import openAiProxyRoutes from "./openai"; + +// The native addon is unavailable: the helper fails open by resolving to null. +vi.mock("@/routes/proxy/utils/toon-native", () => ({ + toonEncodeToolResults: vi.fn(), + initToonNative: vi.fn(), +})); + +describe("LLM proxy with the TOON addon unavailable", () => { + let app: FastifyInstance; + + beforeEach(() => { + vi.mocked(toonEncodeToolResults).mockResolvedValue(null); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + if (app) { + await app.close(); + } + }); + + test("persists toonSkipReason = addon_unavailable when compression is enabled", async ({ + makeAgent, + }) => { + const agent = await makeAgent({ name: "TOON addon unavailable" }); + + vi.spyOn( + proxyUtils.toonConversion, + "shouldApplyToonCompression", + ).mockResolvedValue(true); + + const upstreamRequests: unknown[] = []; + vi.spyOn(openaiAdapterFactory, "createClient").mockImplementation( + () => + ({ + chat: { + completions: { + create: async (request: unknown) => { + upstreamRequests.push(request); + return { + id: "chatcmpl_nonstream", + object: "chat.completion", + created: 1, + model: "gpt-4o", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: "Mocked response", + refusal: null, + }, + finish_reason: "stop", + logprobs: null, + }, + ], + usage: { + prompt_tokens: 100, + completion_tokens: 20, + total_tokens: 120, + }, + }; + }, + }, + }, + }) as never, + ); + + app = Fastify().withTypeProvider(); + app.setValidatorCompiler(validatorCompiler); + app.setSerializerCompiler(serializerCompiler); + await app.register(openAiProxyRoutes); + + const toolResultContent = JSON.stringify({ + files: [{ name: "README.md" }, { name: "src" }], + }); + const response = await app.inject({ + method: "POST", + url: `/v1/openai/${agent.id}/chat/completions`, + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + payload: { + model: "gpt-4o", + messages: [ + { role: "user", content: "What files are in the current directory?" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_123", + type: "function", + function: { + name: "list_files", + arguments: '{"directory": "."}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "call_123", + content: toolResultContent, + }, + ], + }, + }); + + // Fail-open: the request still succeeds and the tool result reaches the + // provider uncompressed. + expect(response.statusCode).toBe(200); + const lastUpstreamRequest = upstreamRequests.at(-1) as { + messages: { role: string; content?: unknown }[]; + }; + const upstreamToolMessage = lastUpstreamRequest.messages.find( + (message) => message.role === "tool", + ); + expect(upstreamToolMessage?.content).toBe(toolResultContent); + + const interactions = await InteractionModel.getAllInteractionsForProfile( + agent.id, + ); + expect(interactions).toHaveLength(1); + expect(interactions[0].toonSkipReason).toBe("addon_unavailable"); + }); +}); diff --git a/platform/backend/src/routes/proxy/utils/toon-native-token-parity.test.ts b/platform/backend/src/routes/proxy/utils/toon-native-token-parity.test.ts new file mode 100644 index 00000000000..f5ca49986bf --- /dev/null +++ b/platform/backend/src/routes/proxy/utils/toon-native-token-parity.test.ts @@ -0,0 +1,87 @@ +// Differential guard on the cl100k parity contract: the native fused counts +// (tiktoken-rs) must equal the JS `tiktoken` tokenizer over the exact strings +// each adapter compares — the golden corpus plus adversarial unicode / +// whitespace / reserved-marker inputs. A tiktoken-rs bump that shifts a single +// token here changes recorded compression stats, so this pins native == JS +// directly rather than only through the per-adapter number pins. + +import { expect, test } from "@/test"; +import { allGoldenEntries, describeNative } from "@/test/toon-golden"; +import { getTokenizer } from "@/tokenizers"; +import { toonEncodeToolResults } from "./toon-native"; + +const countJs = (content: string) => + getTokenizer("openai").countTokens([{ role: "user", content }]); + +// Each adversarial payload is wrapped in encodable JSON so the strings actually +// reach the count comparison (a bare non-JSON string yields encoded=null and is +// never tokenized). The reserved-marker case doubles as the regression for the +// encode_ordinary baseline: counting it on both sides would throw under the old +// `encode()`. +const ADVERSARIAL = [ + { v: "" }, + { v: " \n\t\r " }, + { v: "café résumé naïve Ångström" }, + { v: "日本語 中文 한국어 emoji 🚀🔥✨🎉" }, + { v: "<|endoftext|> reserved <|fim_prefix|> markers <|fim_suffix|>" }, + { v: "a".repeat(5000) }, + { rows: Array.from({ length: 60 }, (_, i) => ({ id: i, name: `row ${i}` })) }, +].map((payload) => JSON.stringify(payload)); + +describeNative("native cl100k counting matches the JS tokenizer", () => { + test("golden corpus + adversarial inputs count identically to JS", async () => { + const inputs = [ + ...allGoldenEntries().map((entry) => ({ + rawContent: entry.rawContent, + unwrap: entry.unwrap, + })), + ...ADVERSARIAL.map((rawContent) => ({ rawContent, unwrap: false })), + ]; + + const results = await toonEncodeToolResults( + inputs.map((input, i) => ({ id: `p${i}`, ...input })), + "normalized", + ); + expect(results).not.toBeNull(); + if (results === null) return; + expect(results).toHaveLength(inputs.length); + + let compared = 0; + for (const result of results) { + if (result.encoded === null) { + // Unencodable items are never tokenized. + expect(result.beforeTokens).toBeNull(); + expect(result.encodedTokens).toBeNull(); + continue; + } + // Counts are taken on the exact post-transform strings the adapter uses, + // so any boundary normalization applies equally to both sides. + expect(result.beforeTokens).toBe(countJs(result.normalized)); + expect(result.encodedTokens).toBe(countJs(result.encoded)); + compared++; + } + // The adversarial payloads are all encodable, so the count comparison must + // actually run on them (not just assert null on non-JSON input). + expect(compared).toBeGreaterThanOrEqual(ADVERSARIAL.length); + }); + + test("the Raw baseline counts the original pre-unwrap serialization", async () => { + // A wrapped payload: Raw counts the whole wrapper (what Gemini tokenizes), + // not the unwrapped inner text that Normalized would. + const inner = JSON.stringify({ data: [{ id: 1 }, { id: 2 }, { id: 3 }] }); + const wrapped = JSON.stringify([{ type: "text", text: inner }]); + + const results = await toonEncodeToolResults( + [{ id: "w", rawContent: wrapped, unwrap: true }], + "raw", + ); + expect(results).not.toBeNull(); + if (results === null) return; + + const [result] = results; + expect(result.encoded).not.toBeNull(); + expect(result.beforeTokens).toBe(countJs(wrapped)); + // Sanity: the raw wrapper is larger than the unwrapped inner text. + expect(countJs(wrapped)).toBeGreaterThan(countJs(inner)); + }); +}); diff --git a/platform/backend/src/routes/proxy/utils/toon-native.golden.test.ts b/platform/backend/src/routes/proxy/utils/toon-native.golden.test.ts new file mode 100644 index 00000000000..8953ce9dce1 --- /dev/null +++ b/platform/backend/src/routes/proxy/utils/toon-native.golden.test.ts @@ -0,0 +1,74 @@ +// Runs the committed TOON v3 golden corpus (generated by the Rust +// proxy-transform-core crate) through the real native addon via the +// toon-native helper, pinning exact normalized/encoded output at the JS +// boundary. Requires the built addon: mandatory in CI (the workspace builds +// native deps before vitest); locally it skips visibly when the addon is not +// built — run `pnpm test:native` from platform/backend. + +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { toonEncodeToolResults } from "./toon-native"; + +type GoldenEntry = { + name: string; + rawContent: string; + unwrap: boolean; + expected: { normalized: string; encoded: string | null }; +}; + +const CORPUS_PATH = path.resolve( + import.meta.dirname, + "../../../../../archestra-rs/proxy-transform-core/tests/fixtures/golden-corpus.json", +); + +const addonLoadError: unknown = await import( + "@archestra/proxy-transform-rs" +).then( + () => null, + (error) => error, +); + +// In CI the suite must FAIL (never skip) when the addon is missing. +const describeNative = + addonLoadError === null || process.env.CI ? describe : describe.skip; +if (addonLoadError !== null && !process.env.CI) { + console.warn( + `[toon-native.golden.test] skipping: @archestra/proxy-transform-rs is not built (${String( + addonLoadError, + )}). Run \`pnpm test:native\` from platform/backend.`, + ); +} + +describeNative("TOON v3 golden corpus through the native helper", () => { + test("the native addon loads", () => { + expect(addonLoadError).toBeNull(); + }); + + test("every corpus entry matches its committed golden exactly", async () => { + const corpus: GoldenEntry[] = JSON.parse(readFileSync(CORPUS_PATH, "utf8")); + expect(corpus.length).toBeGreaterThan(0); + + const results = await toonEncodeToolResults( + corpus.map((entry) => ({ + id: entry.name, + rawContent: entry.rawContent, + unwrap: entry.unwrap, + })), + ); + + expect(results).not.toBeNull(); + // Keyed by entry name so a mismatch reports which fixture diverged. + const actualByName = corpus.map((entry, index) => ({ + name: entry.name, + normalized: results?.[index].normalized, + encoded: results?.[index].encoded, + })); + const expectedByName = corpus.map((entry) => ({ + name: entry.name, + normalized: entry.expected.normalized, + encoded: entry.expected.encoded, + })); + expect(actualByName).toStrictEqual(expectedByName); + }); +}); diff --git a/platform/backend/src/routes/proxy/utils/toon-native.test.ts b/platform/backend/src/routes/proxy/utils/toon-native.test.ts new file mode 100644 index 00000000000..91e755360e5 --- /dev/null +++ b/platform/backend/src/routes/proxy/utils/toon-native.test.ts @@ -0,0 +1,212 @@ +import { toonEncodeToolResults as nativeToonEncodeToolResults } from "@archestra/proxy-transform-rs"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { metrics } from "@/observability"; +import { convertToolResultsToToon } from "../adapters/openai"; +import { toonEncodeToolResults } from "./toon-native"; + +// The native transform is a compiled addon that may be absent in the unit-test +// env; mock it at the boundary to pin the helper's wiring only. Real encoding +// is covered by toon-native.golden.test.ts and the Rust crate tests. +vi.mock("@archestra/proxy-transform-rs", () => ({ + toonEncodeToolResults: vi.fn(), +})); +vi.mock("@/observability"); + +const items = [ + { id: "call_1", rawContent: '{"a":[1,2]}', unwrap: true }, + { id: "call_2", rawContent: "not json", unwrap: true }, +]; + +describe("toonEncodeToolResults helper", () => { + beforeEach(() => { + vi.mocked(nativeToonEncodeToolResults).mockReset(); + }); + + test("returns the positional native results on success", async () => { + const nativeResults = [ + { + normalized: '{"a":[1,2]}', + encoded: "a[2]: 1,2", + beforeTokens: null, + encodedTokens: null, + }, + { + normalized: "not json", + encoded: null, + beforeTokens: null, + encodedTokens: null, + }, + ]; + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue(nativeResults); + + const results = await toonEncodeToolResults(items); + + expect(results).toStrictEqual(nativeResults); + expect(nativeToonEncodeToolResults).toHaveBeenCalledWith(items, undefined); + expect(metrics.llm.reportToonAddonUnavailable).not.toHaveBeenCalled(); + }); + + test("requests native counting and returns the counts", async () => { + const nativeResults = [ + { + normalized: '{"a":[1,2]}', + encoded: "a[2]: 1,2", + beforeTokens: 8, + encodedTokens: 5, + }, + { + normalized: "not json", + encoded: null, + beforeTokens: null, + encodedTokens: null, + }, + ]; + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue(nativeResults); + + const results = await toonEncodeToolResults(items, "normalized"); + + expect(results).toStrictEqual(nativeResults); + // The friendly union maps to the native string_enum value. + expect(nativeToonEncodeToolResults).toHaveBeenCalledWith( + items, + "Normalized", + ); + }); + + test("maps the gemini raw baseline to the native Raw source", async () => { + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue([ + { + normalized: "not json", + encoded: null, + beforeTokens: null, + encodedTokens: null, + }, + ]); + + await toonEncodeToolResults([items[1]], "raw"); + + expect(nativeToonEncodeToolResults).toHaveBeenCalledWith([items[1]], "Raw"); + }); + + test("returns null when a requested count is missing for an encoded item", async () => { + // Native tokenizer unavailable: encoding succeeded but counts came back + // null. The keep/reject decision cannot proceed, so fail open. + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue([ + { + normalized: '{"a":[1,2]}', + encoded: "a[2]: 1,2", + beforeTokens: null, + encodedTokens: null, + }, + ]); + + const results = await toonEncodeToolResults([items[0]], "normalized"); + + expect(results).toBeNull(); + expect(metrics.llm.reportToonAddonUnavailable).toHaveBeenCalledWith( + "request", + ); + }); + + test("returns null when counts appear though none were requested", async () => { + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue([ + { + normalized: '{"a":[1,2]}', + encoded: "a[2]: 1,2", + beforeTokens: 8, + encodedTokens: 5, + }, + ]); + + const results = await toonEncodeToolResults([items[0]]); + + expect(results).toBeNull(); + }); + + test("returns null when the native batch length does not match the input", async () => { + vi.mocked(nativeToonEncodeToolResults).mockResolvedValue([ + { + normalized: '{"a":[1,2]}', + encoded: "a[2]: 1,2", + beforeTokens: null, + encodedTokens: null, + }, + ]); + + const results = await toonEncodeToolResults(items); + + expect(results).toBeNull(); + expect(metrics.llm.reportToonAddonUnavailable).toHaveBeenCalledWith( + "request", + ); + }); + + test("returns null (fail-open) when the native call fails", async () => { + vi.mocked(nativeToonEncodeToolResults).mockRejectedValue( + new Error("addon missing"), + ); + + const results = await toonEncodeToolResults(items); + + expect(results).toBeNull(); + expect(metrics.llm.reportToonAddonUnavailable).toHaveBeenCalledWith( + "request", + ); + }); +}); + +describe("convertToolResultsToToon with the addon unavailable", () => { + beforeEach(() => { + vi.mocked(nativeToonEncodeToolResults).mockReset(); + vi.mocked(nativeToonEncodeToolResults).mockRejectedValue( + new Error("addon missing"), + ); + }); + + test("fails open: messages untouched, explicit addon_unavailable skip reason", async () => { + const messages = [ + { role: "user" as const, content: "list files" }, + { + role: "tool" as const, + tool_call_id: "call_1", + content: '{"files":[{"name":"a"},{"name":"b"}]}', + }, + ]; + + const { messages: resultMessages, stats } = await convertToolResultsToToon( + messages, + "gpt-4o", + "openai", + ); + + expect(resultMessages).toStrictEqual(messages); + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: true, + skipReason: "addon_unavailable", + }); + }); + + test("does not call the native addon when there are no tool results", async () => { + const messages = [{ role: "user" as const, content: "hello" }]; + + const { messages: resultMessages, stats } = await convertToolResultsToToon( + messages, + "gpt-4o", + "openai", + ); + + expect(resultMessages).toStrictEqual(messages); + expect(stats).toStrictEqual({ + tokensBefore: 0, + tokensAfter: 0, + costSavings: 0, + wasEffective: false, + hadToolResults: false, + }); + expect(nativeToonEncodeToolResults).not.toHaveBeenCalled(); + }); +}); diff --git a/platform/backend/src/routes/proxy/utils/toon-native.ts b/platform/backend/src/routes/proxy/utils/toon-native.ts new file mode 100644 index 00000000000..942641f59a3 --- /dev/null +++ b/platform/backend/src/routes/proxy/utils/toon-native.ts @@ -0,0 +1,130 @@ +// Batched TOON encoding of tool results via the native Rust addon +// (@archestra/proxy-transform-rs), off the JS thread. Fail-open: any +// load/call failure resolves to null so adapters skip compression for the +// request and the handler records the explicit `addon_unavailable` skip +// reason instead of fabricating stats. + +import type { + BeforeSource, + ToonEncodeItem, + ToonEncodeResult, +} from "@archestra/proxy-transform-rs"; +import logger from "@/logging"; +import { metrics } from "@/observability"; + +export type { ToonEncodeItem, ToonEncodeResult }; + +/** + * Which string the adapter tokenizes as the pre-compression baseline, so the + * native pass returns matching `beforeTokens`/`encodedTokens`: `"normalized"` + * (post-unwrap) for most adapters, `"raw"` (the original serialization) for + * Gemini. Omit it to skip counting (Anthropic and Bedrock count with their own + * tokenizer). Adapters pass the string literal, so this stays module-local. + */ +type ToonBeforeSource = "raw" | "normalized"; + +// The native binding types `beforeSource` as a `const enum` (string values +// "Raw"/"Normalized"). esbuild/tsx cannot inline a const enum across modules, +// so adapters must not reference its members; instead they pass this friendly +// union and the underlying string values are supplied here, at the one call +// site, matched to the enum's own values. +const NATIVE_BEFORE_SOURCE: Record = { + raw: "Raw" as unknown as BeforeSource, + normalized: "Normalized" as unknown as BeforeSource, +}; + +/** + * Transform a batch of tool results (optional client-wrapper unwrap → JSON + * parse → TOON encode). Results are positional — same order and length as + * `items`; `encoded` is null for content that is not parseable JSON. + * + * When `beforeSource` is given, the native pass also returns the cl100k token + * counts (`beforeTokens`/`encodedTokens`) that gate the keep/reject decision, + * off the event loop. They are populated for every encodable item; a requested + * count coming back absent means the native tokenizer is unavailable, treated + * the same as a missing addon (return null, skip compression). + * + * Returns null when the native addon is unavailable or misbehaves (callers + * must then skip compression entirely and surface `addon_unavailable`). + */ +export async function toonEncodeToolResults( + items: ToonEncodeItem[], + beforeSource?: ToonBeforeSource, +): Promise { + try { + const native = await loadProxyTransformNative(); + const results = await native.toonEncodeToolResults( + items, + beforeSource ? NATIVE_BEFORE_SOURCE[beforeSource] : undefined, + ); + if (results.length !== items.length) { + throw new Error( + `native toonEncodeToolResults returned ${results.length} results for ${items.length} items`, + ); + } + assertTokenCountInvariant(results, beforeSource !== undefined); + return results; + } catch (error) { + logger.error( + { err: error, itemCount: items.length }, + "[toon-native] native TOON encode failed — skipping tool result compression for this request", + ); + metrics.llm.reportToonAddonUnavailable("request"); + return null; + } +} + +/** + * Eager startup probe: load the addon once so a broken deployment surfaces at + * boot (error log + metric) instead of silently skipping compression per + * request. Never throws — the proxy fails open. + */ +export async function initToonNative(): Promise { + try { + await loadProxyTransformNative(); + logger.info("[toon-native] native proxy-transform addon loaded"); + } catch (error) { + logger.error( + { err: error }, + "[toon-native] failed to load @archestra/proxy-transform-rs at startup — TOON compression will be skipped (addon_unavailable)", + ); + metrics.llm.reportToonAddonUnavailable("startup"); + } +} + +// Guard the token-count contract at the boundary: a requested count that comes +// back absent, or a stray count when none was requested, would silently corrupt +// the keep/reject decision (NaN comparisons, bogus stats). Throwing routes it +// into the fail-open path (return null → addon_unavailable) instead. +function assertTokenCountInvariant( + results: ToonEncodeResult[], + counted: boolean, +): void { + for (const { encoded, beforeTokens, encodedTokens } of results) { + if (counted && encoded !== null) { + if (!isCount(beforeTokens) || !isCount(encodedTokens)) { + throw new Error( + "native toonEncodeToolResults omitted token counts for an encoded item", + ); + } + } else if (beforeTokens !== null || encodedTokens !== null) { + throw new Error( + "native toonEncodeToolResults returned token counts that were not requested", + ); + } + } +} + +function isCount(value: number | null): value is number { + return value !== null && Number.isInteger(value) && value >= 0; +} + +// Lazy, memoized load of the native addon: codegen and paths that never +// compress tool results don't require the built `.node`. Mirrors +// utils/image-conversion.ts and the sandbox/app-runtime native loaders. +type ProxyTransformBindings = typeof import("@archestra/proxy-transform-rs"); +let nativeBindings: Promise | null = null; +function loadProxyTransformNative(): Promise { + nativeBindings ??= import("@archestra/proxy-transform-rs"); + return nativeBindings; +} diff --git a/platform/backend/src/routes/proxy/utils/unwrap-tool-content.test.ts b/platform/backend/src/routes/proxy/utils/unwrap-tool-content.test.ts deleted file mode 100644 index 6caf0238641..00000000000 --- a/platform/backend/src/routes/proxy/utils/unwrap-tool-content.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, test } from "@/test"; -import { unwrapToolContent } from "./unwrap-tool-content"; - -describe("unwrapToolContent", () => { - test("unwraps stringified array with text block wrapper", () => { - const wrapped = '[{"type":"text","text":"{\\"data\\":\\"value\\"}"}]'; - const result = unwrapToolContent(wrapped); - expect(result).toBe('{"data":"value"}'); - }); - - test("unwraps array object with text block wrapper", () => { - const wrapped = [{ type: "text", text: '{"data":"value"}' }]; - const result = unwrapToolContent(wrapped); - expect(result).toBe('{"data":"value"}'); - }); - - test("returns plain JSON string unchanged", () => { - const plain = '{"data":"value"}'; - const result = unwrapToolContent(plain); - expect(result).toBe('{"data":"value"}'); - }); - - test("returns non-JSON string unchanged", () => { - const plain = "just plain text"; - const result = unwrapToolContent(plain); - expect(result).toBe("just plain text"); - }); - - test("handles empty array", () => { - const empty = "[]"; - const result = unwrapToolContent(empty); - expect(result).toBe("[]"); - }); - - test("handles array without type:text", () => { - const noType = '[{"foo":"bar"}]'; - const result = unwrapToolContent(noType); - expect(result).toBe('[{"foo":"bar"}]'); - }); - - test("handles array with type but no text field", () => { - const noText = '[{"type":"text","content":"value"}]'; - const result = unwrapToolContent(noText); - expect(result).toBe('[{"type":"text","content":"value"}]'); - }); - - test("unwraps complex nested JSON", () => { - const wrapped = - '[{"type":"text","text":"{\\"issues\\":[{\\"id\\":123,\\"title\\":\\"Test\\"}]}"}]'; - const result = unwrapToolContent(wrapped); - expect(result).toBe('{"issues":[{"id":123,"title":"Test"}]}'); - }); - - test("handles object input (non-string)", () => { - const obj = { data: "value" }; - const result = unwrapToolContent(obj); - expect(result).toBe('{"data":"value"}'); - }); - - test("unwraps when given as object array", () => { - const wrapped = [{ type: "text", text: '{"issues":[{"id":123}]}' }]; - const result = unwrapToolContent(wrapped); - expect(result).toBe('{"issues":[{"id":123}]}'); - }); - - test("returns unwrapped content from multiple wrapper formats", () => { - // String format - const stringWrapped = - '[{"type":"text","text":"{\\"temperature\\":20,\\"condition\\":\\"sunny\\"}"}]'; - expect(unwrapToolContent(stringWrapped)).toBe( - '{"temperature":20,"condition":"sunny"}', - ); - - // Array format - const arrayWrapped = [ - { type: "text", text: '{"temperature":20,"condition":"sunny"}' }, - ]; - expect(unwrapToolContent(arrayWrapped)).toBe( - '{"temperature":20,"condition":"sunny"}', - ); - }); -}); diff --git a/platform/backend/src/routes/proxy/utils/unwrap-tool-content.ts b/platform/backend/src/routes/proxy/utils/unwrap-tool-content.ts deleted file mode 100644 index 64d7b1fd349..00000000000 --- a/platform/backend/src/routes/proxy/utils/unwrap-tool-content.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Unwrap extra text block wrapping from tool result content. - * - * Some clients (like n8n, Vercel AI SDK) wrap tool results in a text block structure: - * - Input: "[{\"type\":\"text\",\"text\":\"{\\\"data\\\":...}\"}]" - * - Output: "{\"data\":...}" - * - * Or as an array: - * - Input: [{"type":"text","text":"{\"data\":...}"}] - * - Output: "{\"data\":...}" - * - * This is necessary for TOON conversion which expects the raw JSON string, - * not wrapped in additional structures. - */ -export function unwrapToolContent(content: string | unknown): string { - // Convert to string if it's not already - const contentStr = - typeof content === "string" ? content : JSON.stringify(content); - - try { - const parsed = JSON.parse(contentStr); - - // Check for wrapper format: [{"type":"text","text":"..."}] - if ( - Array.isArray(parsed) && - parsed.length > 0 && - parsed[0]?.type === "text" && - typeof parsed[0]?.text === "string" - ) { - // Return the unwrapped text content - return parsed[0].text; - } - - // Not wrapped, return as-is - return contentStr; - } catch { - // Not valid JSON, return as-is - return contentStr; - } -} diff --git a/platform/backend/src/server.ts b/platform/backend/src/server.ts index 6040577970f..52b98a8357b 100644 --- a/platform/backend/src/server.ts +++ b/platform/backend/src/server.ts @@ -104,6 +104,7 @@ import websocketService from "@/websocket"; import * as routes from "./routes"; import { publicConfigRoutes } from "./routes/config"; import { createOAuthAwareCorsDelegate } from "./routes/oauth-cors"; +import { initToonNative } from "./routes/proxy/utils/toon-native"; import { CONNECTION_SETUP_SCRIPT_PREFIX, HEALTH_PATH, @@ -1128,6 +1129,10 @@ const startWebServer = async () => { const labelKeys = await initializeObservabilityMetrics(); + // Eagerly load the native proxy-transform addon so a broken deployment + // surfaces at boot (error log + metric); the proxy itself fails open. + await initToonNative(); + // Start metrics server await startMetricsServer(); @@ -1488,6 +1493,10 @@ const startWorker = async () => { includeAgentExecutionMetrics: false, }); + // Worker mode registers LLM proxy routes too (registerWorkerRoutes below), + // so eagerly load the native proxy-transform addon here as well. + await initToonNative(); + registerTaskHandlers(taskQueueService); await taskQueueService.seedPeriodicTasks(); taskQueueService.startWorker(); diff --git a/platform/backend/src/test/toon-golden.ts b/platform/backend/src/test/toon-golden.ts new file mode 100644 index 00000000000..71252d68ac5 --- /dev/null +++ b/platform/backend/src/test/toon-golden.ts @@ -0,0 +1,85 @@ +// Shared harness for the per-adapter TOON golden suites +// (src/routes/proxy/adapters/*-toon-compression.test.ts): golden-corpus +// loading, the addon-required skip/fail gate, deterministic pricing, and +// per-provider token counting. Adapter-specific semantics stay in the suites. + +import { readFileSync } from "node:fs"; +import path from "node:path"; +import type { SupportedProvider } from "@archestra/shared"; +import { ModelModel } from "@/models"; +import { describe } from "@/test"; +import { getTokenizer } from "@/tokenizers"; + +export type GoldenEntry = { + name: string; + rawContent: string; + unwrap: boolean; + expected: { normalized: string; encoded: string | null }; +}; + +export function corpusEntry(name: string): GoldenEntry { + const entry = corpus.find((candidate) => candidate.name === name); + if (!entry) throw new Error(`golden corpus entry not found: ${name}`); + return entry; +} + +/** Every golden entry — for suites that sweep the whole corpus. */ +export function allGoldenEntries(): GoldenEntry[] { + return corpus; +} + +export function makeCountTokens(provider: SupportedProvider) { + const tokenizer = getTokenizer(provider); + return (content: string) => + tokenizer.countTokens([{ role: "user", content }]); +} + +// $1,000,000 per million input tokens = $1 per token, so expected costSavings +// equals tokens saved exactly. +export async function upsertOneDollarPerTokenPricing(params: { + provider: SupportedProvider; + modelId: string; +}) { + await ModelModel.upsert({ + externalId: `${params.provider}/${params.modelId}`, + provider: params.provider, + modelId: params.modelId, + inputModalities: null, + outputModalities: null, + customPricePerMillionInput: "1000000.00", + customPricePerMillionOutput: "1000000.00", + lastSyncedAt: new Date(), + }); +} + +export function makeToolCall(id: string, name: string) { + return { + id, + type: "function" as const, + function: { name, arguments: '{"directory":"."}' }, + }; +} + +const CORPUS_PATH = path.resolve( + import.meta.dirname, + "../../../archestra-rs/proxy-transform-core/tests/fixtures/golden-corpus.json", +); +const corpus: GoldenEntry[] = JSON.parse(readFileSync(CORPUS_PATH, "utf8")); + +const addonLoadError: unknown = await import( + "@archestra/proxy-transform-rs" +).then( + () => null, + (error) => error, +); + +// In CI the golden suites must FAIL (never skip) when the addon is missing. +export const describeNative = + addonLoadError === null || process.env.CI ? describe : describe.skip; +if (addonLoadError !== null && !process.env.CI) { + console.warn( + `[toon golden suites] skipping: @archestra/proxy-transform-rs is not built (${String( + addonLoadError, + )}). Run \`pnpm test:native\` from platform/backend.`, + ); +} diff --git a/platform/backend/src/tokenizers/tiktoken.ts b/platform/backend/src/tokenizers/tiktoken.ts index 3b19a07aa7d..a62ff82b685 100644 --- a/platform/backend/src/tokenizers/tiktoken.ts +++ b/platform/backend/src/tokenizers/tiktoken.ts @@ -16,6 +16,11 @@ export class TiktokenTokenizer extends BaseTokenizer { } protected computeMessageTokens(encodableText: string): number { - return this.encoding.encode(encodableText).length; + // Ordinary encoding: treat reserved-marker literals (e.g. `<|endoftext|>`) + // that appear in tool results as plain text. `encode()` raises on them by + // default, which would crash counting on otherwise-valid content; the + // native cl100k path (proxy-transform-core) counts ordinally too, so this + // keeps the two byte-identical. + return this.encoding.encode_ordinary(encodableText).length; } } diff --git a/platform/backend/src/tokenizers/tokenizers.test.ts b/platform/backend/src/tokenizers/tokenizers.test.ts index 9c14ab872cf..f91bd75bed2 100644 --- a/platform/backend/src/tokenizers/tokenizers.test.ts +++ b/platform/backend/src/tokenizers/tokenizers.test.ts @@ -60,6 +60,21 @@ describe("Tokenizers", () => { // Should at least count the role expect(tokenCount).toBeGreaterThanOrEqual(0); }); + + test("counts reserved-marker literals as text instead of throwing", () => { + // A tool result can contain `<|endoftext|>` verbatim; the default + // `encode()` raises on reserved markers, which would crash counting. + // Ordinary encoding counts them as plain text. + const tokenizer = new TiktokenTokenizer(); + const message: ProviderMessage = { + role: "user", + content: "row 1 <|endoftext|> row 2 <|fim_prefix|>", + }; + + const tokenCount = tokenizer.countTokens(message); + + expect(tokenCount).toBeGreaterThan(0); + }); }); describe("AnthropicTokenizer", () => { diff --git a/platform/backend/src/types/interaction.ts b/platform/backend/src/types/interaction.ts index 56224e2eb8f..69307602189 100644 --- a/platform/backend/src/types/interaction.ts +++ b/platform/backend/src/types/interaction.ts @@ -505,6 +505,7 @@ export const ToonSkipReasonCountsSchema = z.object({ notEnabled: z.number(), notEffective: z.number(), noToolResults: z.number(), + addonUnavailable: z.number(), }); /** diff --git a/platform/backend/src/types/llm-provider.ts b/platform/backend/src/types/llm-provider.ts index 772e0e699c1..41b15ecf44e 100644 --- a/platform/backend/src/types/llm-provider.ts +++ b/platform/backend/src/types/llm-provider.ts @@ -312,7 +312,23 @@ export interface LLMStreamAdapter { */ formatTextDeltaSSE(text: string): string | Uint8Array; - /** Get raw tool call events as SSE strings (for replay after policy approval) */ + /** + * Get raw tool call events as SSE-encoded payloads, for replay after tool + * invocation policy evaluation. + * + * Contract (the streaming proxy handler depends on every point — it calls + * this after every tool-call chunk and again at final flush, deduplicating + * replayed events by ARRAY INDEX): + * - Returns the FULL event history accumulated so far, in arrival order. + * - Append-only: an event's index never changes across calls, and events + * are never dropped or re-based. + * - Non-destructive: calling this any number of times must not change what + * subsequent calls return. + * + * Because the getter runs once per tool-call chunk, implementations should + * serialize each event once at accumulation time and return the cached + * encodings; re-serializing the whole history per call is O(k^2) per stream. + */ getRawToolCallEvents(): (string | Uint8Array)[]; /** diff --git a/platform/backend/src/types/tool-result-compression.ts b/platform/backend/src/types/tool-result-compression.ts index dc24e666c76..0f8d78c8bc7 100644 --- a/platform/backend/src/types/tool-result-compression.ts +++ b/platform/backend/src/types/tool-result-compression.ts @@ -4,6 +4,7 @@ export const ToonSkipReasonSchema = z.enum([ "not_enabled", "not_effective", "no_tool_results", + "addon_unavailable", ]); export type ToonSkipReason = z.infer; @@ -21,6 +22,13 @@ export interface ToolCompressionStats { wasEffective: boolean; /** Whether there were any tool results to compress */ hadToolResults: boolean; + /** + * Set when compression could not run at all (native addon unavailable — + * an infrastructure failure, not an outcome of trying). Takes precedence + * over the reasons the handler derives from the count fields, so the + * interaction is never misreported as not_effective/no_tool_results. + */ + skipReason?: Extract; } /** diff --git a/platform/biome.json b/platform/biome.json index 5302eedd4d7..f3cc5276553 100644 --- a/platform/biome.json +++ b/platform/biome.json @@ -21,7 +21,8 @@ "!helm/**/*", "!**/*.gen.ts", "!**/sandbox-rs/index.d.ts", - "!**/app-runtime-rs/index.d.ts" + "!**/app-runtime-rs/index.d.ts", + "!**/proxy-transform-rs/index.d.ts" ] }, "formatter": { diff --git a/platform/dev/Tiltfile.dev b/platform/dev/Tiltfile.dev index 3446de405bb..d2c39127ec3 100644 --- a/platform/dev/Tiltfile.dev +++ b/platform/dev/Tiltfile.dev @@ -73,6 +73,8 @@ if code_runtime_enabled: backend_resource_deps.append('sandbox-rs-build') # The MCP App runtime addon must be built before the backend boots. backend_resource_deps.append('app-runtime-rs-build') +# The LLM proxy's TOON transform addon must be built before the backend boots. +backend_resource_deps.append('proxy-transform-rs-build') local_resource( 'pnpm-install', @@ -106,6 +108,24 @@ local_resource( labels=['apps'], ) +# Rebuild the native proxy-transform addon (napi) when its Rust sources change. +# The LLM proxy's TOON compression loads it, so it must exist before the +# backend boots. Mirrors app-runtime-rs-build. +local_resource( + 'proxy-transform-rs-build', + cmd='pnpm --filter @archestra/proxy-transform-rs build:dev', + deps=[ + '../archestra-rs/proxy-transform-rs/src', + '../archestra-rs/proxy-transform-core/src', + '../archestra-rs/proxy-transform-rs/Cargo.toml', + '../archestra-rs/proxy-transform-core/Cargo.toml', + '../archestra-rs/Cargo.toml', + '../archestra-rs/Cargo.lock', + ], + resource_deps=['pnpm-install'], + labels=['dev'], +) + # Main dev/prod bundle if is_prod: local_resource( diff --git a/platform/frontend/src/components/savings.tsx b/platform/frontend/src/components/savings.tsx index 2944f1ec307..b6e9319c4a0 100644 --- a/platform/frontend/src/components/savings.tsx +++ b/platform/frontend/src/components/savings.tsx @@ -140,6 +140,11 @@ export function Savings({
Tool result compression: Skipped (no token savings)
) : toonSkipReason === "no_tool_results" ? (
Tool result compression: No tool results
+ ) : toonSkipReason === "addon_unavailable" ? ( +
+ Tool result compression: Not applied (compression engine + unavailable) +
) : (
Tool result compression: Not applied
)} diff --git a/platform/frontend/src/mocks/data/interactions.ts b/platform/frontend/src/mocks/data/interactions.ts index 74d0fdc8a12..d5695a36179 100644 --- a/platform/frontend/src/mocks/data/interactions.ts +++ b/platform/frontend/src/mocks/data/interactions.ts @@ -51,6 +51,7 @@ export function makeSessionSummary( notEnabled: 0, notEffective: 0, noToolResults: 0, + addonUnavailable: 0, }, firstRequestTime: "2026-01-01T00:00:00.000Z", lastRequestTime: "2026-01-01T00:00:00.000Z", diff --git a/platform/pnpm-lock.yaml b/platform/pnpm-lock.yaml index b5ba94cd369..a1c73c01433 100644 --- a/platform/pnpm-lock.yaml +++ b/platform/pnpm-lock.yaml @@ -129,6 +129,16 @@ importers: archestra-rs/napi-loader: {} + archestra-rs/proxy-transform-rs: + dependencies: + '@archestra/napi-loader': + specifier: workspace:* + version: link:../napi-loader + devDependencies: + '@napi-rs/cli': + specifier: ^3.4.0 + version: 3.6.2(@emnapi/runtime@1.8.1)(@types/node@25.9.1) + archestra-rs/sandbox-rs: dependencies: '@archestra/napi-loader': @@ -183,6 +193,9 @@ importers: '@archestra/image-rs': specifier: workspace:* version: link:../archestra-rs/image-rs + '@archestra/proxy-transform-rs': + specifier: workspace:* + version: link:../archestra-rs/proxy-transform-rs '@archestra/sandbox-rs': specifier: workspace:* version: link:../archestra-rs/sandbox-rs @@ -348,9 +361,6 @@ importers: '@smithy/util-utf8': specifier: ^4.2.2 version: 4.2.2 - '@toon-format/toon': - specifier: ^2.1.0 - version: 2.1.0 '@types/pdf-parse': specifier: ^1.1.5 version: 1.1.5 @@ -5855,9 +5865,6 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@toon-format/toon@2.1.0': - resolution: {integrity: sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg==} - '@turbo/darwin-64@2.9.14': resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} cpu: [x64] @@ -16556,8 +16563,6 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@toon-format/toon@2.1.0': {} - '@turbo/darwin-64@2.9.14': optional: true diff --git a/platform/pnpm-workspace.yaml b/platform/pnpm-workspace.yaml index 104a1c35b91..118262b772d 100644 --- a/platform/pnpm-workspace.yaml +++ b/platform/pnpm-workspace.yaml @@ -7,6 +7,7 @@ packages: - "archestra-rs/sandbox-rs" - "archestra-rs/app-runtime-rs" - "archestra-rs/image-rs" + - "archestra-rs/proxy-transform-rs" - "e2e-tests" catalog: diff --git a/platform/scripts/dev-stack.sh b/platform/scripts/dev-stack.sh index ad77fb2b2d5..30d45382e99 100755 --- a/platform/scripts/dev-stack.sh +++ b/platform/scripts/dev-stack.sh @@ -58,24 +58,25 @@ require_platform_cwd() { fi } -# The backend lazily loads three NAPI addons (@archestra/app-runtime-rs, -# sandbox-rs, image-rs) whose compiled `.node` binaries are gitignored and are -# never built by `pnpm install` (install scripts are disabled repo-wide), so a -# fresh worktree fails with "Unable to load @archestra/ for " -# the first time an app/sandbox/image feature is touched. Build the missing -# ones before launching Tilt. Skip crates whose binary already exists — a +# The backend lazily loads four NAPI addons (@archestra/app-runtime-rs, +# sandbox-rs, image-rs, proxy-transform-rs) whose compiled `.node` binaries are +# gitignored and are never built by `pnpm install` (install scripts are +# disabled repo-wide), so a fresh worktree fails with "Unable to load +# @archestra/ for " the first time an +# app/sandbox/image/LLM-proxy feature is touched. Build the missing ones +# before launching Tilt. Skip crates whose binary already exists — a # stale-but-present binary is rebuilt manually via `pnpm --filter build` # — so restarts stay fast. ensure_native_addons() { local platform_dir="$1" crate filters=() - for crate in app-runtime-rs sandbox-rs image-rs; do + for crate in app-runtime-rs sandbox-rs image-rs proxy-transform-rs; do if ! ls "$platform_dir/archestra-rs/$crate"/*.node >/dev/null 2>&1; then filters+=("--filter" "@archestra/$crate") fi done [ ${#filters[@]} -eq 0 ] && return 0 if ! command -v cargo >/dev/null 2>&1; then - echo "⚠ Rust toolchain not found; skipping native addon build. App/sandbox/image features will fail until you install Rust and run: pnpm --filter '@archestra/*-rs' build" >&2 + echo "⚠ Rust toolchain not found; skipping native addon build. App/sandbox/image/LLM-proxy features will fail until you install Rust and run: pnpm --filter '@archestra/*-rs' build" >&2 return 0 fi echo "→ Building missing native addons (first build takes a few minutes): ${filters[*]}" >&2 diff --git a/platform/shared/hey-api/clients/api/types.gen.ts b/platform/shared/hey-api/clients/api/types.gen.ts index 07c34da0d26..618ff158a2d 100644 --- a/platform/shared/hey-api/clients/api/types.gen.ts +++ b/platform/shared/hey-api/clients/api/types.gen.ts @@ -32157,7 +32157,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -32351,7 +32351,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -32460,7 +32460,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -32567,7 +32567,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -32652,7 +32652,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -32739,7 +32739,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33250,7 +33250,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33337,7 +33337,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33424,7 +33424,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33511,7 +33511,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33598,7 +33598,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33685,7 +33685,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33772,7 +33772,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33859,7 +33859,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -33944,7 +33944,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -34029,7 +34029,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -34116,7 +34116,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -34203,7 +34203,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -34931,7 +34931,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -35659,7 +35659,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -35746,7 +35746,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -35900,7 +35900,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -36094,7 +36094,7 @@ export type GetInteractionsResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -36266,6 +36266,7 @@ export type GetInteractionSessionsResponses = { notEnabled: number; notEffective: number; noToolResults: number; + addonUnavailable: number; }; firstRequestTime: string; lastRequestTime: string; @@ -36598,7 +36599,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -36792,7 +36793,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -36901,7 +36902,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37008,7 +37009,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37093,7 +37094,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37180,7 +37181,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37691,7 +37692,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37778,7 +37779,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37865,7 +37866,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -37952,7 +37953,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38039,7 +38040,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38126,7 +38127,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38213,7 +38214,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38300,7 +38301,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38385,7 +38386,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38470,7 +38471,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38557,7 +38558,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -38644,7 +38645,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -39372,7 +39373,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -40100,7 +40101,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -40187,7 +40188,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -40341,7 +40342,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; @@ -40535,7 +40536,7 @@ export type GetInteractionResponses = { toonTokensBefore: number | null; toonTokensAfter: number | null; toonCostSavings: string | null; - toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results'; + toonSkipReason?: 'not_enabled' | 'not_effective' | 'no_tool_results' | 'addon_unavailable'; createdAt: string; chatErrors?: Array<{ id: string; diff --git a/platform/turbo.json b/platform/turbo.json index 5ef58feaaeb..17d69db9177 100644 --- a/platform/turbo.json +++ b/platform/turbo.json @@ -20,6 +20,10 @@ "dependsOn": ["^build"], "cache": false }, + "@archestra/proxy-transform-rs#build": { + "dependsOn": ["^build"], + "cache": false + }, "dev": { "persistent": true, "cache": false, @@ -42,7 +46,7 @@ "inputs": ["$TURBO_DEFAULT$", "**/*.tsx", "**/*.ts", "./biome.json", "**/knip.config.ts", "**/package.json", "src/database/migrations/**"] }, "@backend#check:ci": { - "dependsOn": ["@archestra/app-runtime-rs#build", "@archestra/sandbox-rs#build"] + "dependsOn": ["@archestra/app-runtime-rs#build", "@archestra/sandbox-rs#build", "@archestra/proxy-transform-rs#build"] }, "check:commit": { "inputs": ["$TURBO_DEFAULT$", "**/*.tsx", "**/*.ts", "./biome.json", "**/knip.config.ts", "**/package.json", "src/database/migrations/**"]