Skip to content

fix(ai-providers): align CCA schema sanitization with oh-my-pi (fail-soft gate) - #906

Open
JS-banana wants to merge 2 commits into
openpi-dev:mainfrom
JS-banana:fix/antigravity-const-schema
Open

JS-banana wants to merge 2 commits into
openpi-dev:mainfrom
JS-banana:fix/antigravity-const-schema

Conversation

@JS-banana

Copy link
Copy Markdown
Contributor

Stacked on #905 — the first commit of this branch is that PR's; only 7e18c1f9 is new here. If #905 merges first, this PR's diff collapses to the hardening commit.

Problem

sanitizeSchemaForCca is a denylist snapshot of oh-my-pi's older fields.ts. Two gaps remain after the const fix (#905): (1) nine Google-dialect rejected keywords are missing from the denylist ($id, $anchor, contains, minContains, maxContains, additionalItems, contentEncoding, contentMediaType, contentSchema), so tools carrying them still 400; (2) the denylist shape fails hard — any keyword outside the list reaches the wire, and one rejection kills every request in the session. That failure class has now recurred twice on this sanitizer (#465, #905); out-of-tree schemas (MCP tools, third-party extensions) make recurrence a matter of time, not chance.

Value

Aligns the sanitizer with the reference implementation for the same endpoint (oh-my-pi packages/ai/src/utils/schema, MIT — shipped and exercised daily against Cloud Code Assist) and converts the boundary from fail-hard to fail-soft: a novel schema keyword now degrades the advertised schema instead of 400ing the session.

Approach

Four changes to sanitizeSchemaForCca, ported from omp's current pipeline where noted (hand-ported into our single-file sanitizer — no omp dependency, and none of omp's fork-level machinery):

  • Collapse all-literal combinators (anyOf/oneOf/allOf where every variant carries const, e.g. TypeBox Type.Union([Type.Literal(...)])) into a flat enum with a single type, instead of anyOf of singleton enums (omp normalize.ts "allHaveConst"). For allOf the collapse widens an intersection to a union, which only broadens what the model may pick — sound for an advertised schema.
  • Infer a bare const node's type from its value (string/number/boolean).
  • Sync the nine missing denylist entries from omp fields.ts (mechanism unchanged: liftable constraints still spill into description).
  • Add a final allowlist pass: only keys omp's Google/CCA dialect ships through this endpoint today reach the wire (type, title, description, enum, items, properties, required, anyOf, oneOf, allOf); type arrays are scalarized (a null-only array yields no type). Anything else is dropped — fail-soft, mirroring omp's validateAndFallback intent.

Intentionally not ported: omp's $ref/$defs dereferencing, 2020-12 dialect upgrade, nullable extraction, property ordering, and the whole-profile machinery (~2.5k lines); also omp's parametersJsonSchema route for Gemini models — both routes here stay on parameters, verified live.

Validation

  • node --test --experimental-strip-types tests/extensions/ai-providers/antigravity.test.ts: 68 pass, covering combinator collapse (anyOf/oneOf/allOf, with and without explicit types), mixed-type unions staying unions, bare-const inference, the synced denylist entries, the gate (drops if/then/nullable, keeps title per omp parity), type-array scalarization, and properties named const/anyOf; plus a buildRequestBody end-to-end assertion that a literal union reaches the wire as a flat enum.
  • Live against google-antigravity/gemini-3.8-flash-high with subagents+workflows+setup loaded: reply OK; a model-driven subagent_check round-trip succeeds through the collapsed enum.
  • bun run check: clean (biome format/lint, tsc --noEmit).
  • Full suite (node scripts/run-tests.mjs): the same 2 pre-existing tests/extensions/code-mode/index.test.ts failures as clean main.

Impact

  • Model-visible context/tools: literal unions are advertised as one flat enum (same value set, fewer tokens).
  • Wire behavior change for out-of-tree tools: keywords omp does not ship through this endpoint (if/then/else/not, nullable, custom x-*) are now dropped from the advertised schema instead of being sent; previously they were sent and 400ed whenever a tool actually carried them. In-tree tools use none of these today (verified by grep), so there is no in-tree regression.
  • No config, persistence, or lifecycle change. Reverting this PR does not reopen the const 400 fixed by fix(ai-providers): rewrite const schema literals as enum for Cloud Code Assist #905.

…de Assist

TypeBox serializes Type.Union([Type.Literal(...)]) tool parameters as
anyOf branches carrying "const". Cloud Code Assist validates function
declarations against OpenAPI 3.0, which has no "const", so every request
from a session with such a tool 400s with "Unknown name \"const\" at
...parameters.properties[N].value.any_of[M]" (e.g. subagent_check.evidence
since openpi-dev#652; setup's role/browser unions are affected too). enum-carrying
parameters in the same payload pass, so rewrite const to a single-value
enum in sanitizeSchemaForCca instead of dropping the constraint.

Verified live: pi -ne -a -e ai-providers -e subagents against
google-antigravity/gemini-3.8-flash-high 400s before, replies after the
rewrite; broader load with workflows+setup passes as well. Unit tests
cover anyOf-branch literals, enum merging, and a property named const.
Align sanitizeSchemaForCca with oh-my-pi's current schema pipeline
(packages/ai/src/utils/schema, MIT) for the same wire path:

- Collapse all-literal combinators (anyOf/oneOf/allOf where every variant
  carries const, e.g. TypeBox Type.Union([Type.Literal(...)])) into a flat
  enum with a single inferred type, instead of anyOf-of-singleton-enums.
- Infer the type of a bare const node from its value.
- Sync nine missing Google-dialect denylist entries from omp fields.ts:
  $id, $anchor, contains, minContains, maxContains, additionalItems,
  contentEncoding, contentMediaType, contentSchema.
- Add a final allowlist pass: only keys CCA's parameters dialect has
  accepted in practice (type/description/enum/items/properties/required/
  anyOf/oneOf/allOf) reach the wire; type arrays are scalarized. A novel
  schema keyword now degrades the advertised schema (fail-soft) instead
  of 400ing every request, mirroring omp's validateAndFallback.

Verified live against google-antigravity/gemini-3.8-flash-high with
subagents + workflows + setup loaded: plain reply OK, wide smoke OK, and
a real subagent_check tool round-trip through the collapsed enum.
Unit tests 68/68.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant