## What & why
Resolves [OSS-132](https://linear.app/copilotkit/issue/OSS-132).
Investigated with systematic-debugging; every conclusion verified
against the **real** OpenAI Responses API.
**Net change: a TanStack version bump only.** No showcase schema change.
- `@tanstack/ai` `0.18.0` → `0.35.0`
- `@tanstack/ai-openai` `0.9.1` → `0.15.6`
- `package-lock.json` regenerated (Dockerfile uses `npm ci
--legacy-peer-deps`)
## The bug
The built-in-agent showcase 400s on every prompt against real OpenAI.
The state tools (`AGUISendStateSnapshot` / `AGUISendStateDelta` /
`set_steps`) declare arbitrary payloads as `z.any()`, which serializes
to a **typeless** JSON-Schema property (`{ "description": ... }`, no
`"type"`).
The old `@tanstack/openai-base`'s `isStrictModeCompatible()` only
screened for `oneOf/allOf/not/$ref/$defs`, so it missed the missing
`type`, sent the tool with `strict: true`, and OpenAI rejected it:
```
400 Invalid schema for function 'AGUISendStateSnapshot':
In context=('properties','snapshot'), schema must have a 'type' key.
```
This was **masked in production** because the deployed showcase runs
against aimock, which replays fixtures without validating the request
schema — a raw `curl` to prod returns a clean `RUN_FINISHED`, green for
the wrong reason.
The ticket's original framing (zod3/zod4 drift → typeless *root*, `got
"None"`) was already fixed by the zod-4 migration; this is the same
symptom one layer down (typeless *property*).
## The fix is upstream
`@tanstack/ai-openai@0.15.6` (via `@tanstack/openai-base@0.9.2`) fixes
`isStrictModeCompatible`: it now detects typeless / `z.any()` properties
and sends `strict: false`. OpenAI accepts typeless properties under
`strict: false` — so `z.any()` works again with no schema change on our
side.
(`@tanstack/ai-openai@0.15.5` also dropped `@tanstack/ai-client` from
its peerDependencies, so no `ai-client` dep is added.)
## Verification (real OpenAI, gpt-4o)
| Probe | Result |
|---|---|
| Typeless property, `strict: true` (raw OpenAI) | **400** — `schema
must have a 'type' key` |
| Typeless property, `strict: false` (raw OpenAI) | **ACCEPTED** —
confirms it was the strict flag, not the schema |
| `z.any()` tool on old adapter (0.9.1/0.15.4) | adapter sends `strict:
true` → **400** |
| `z.any()` tool on new adapter (0.15.6) | adapter sends **`strict:
false`** → **ACCEPTED**, model calls the tool |
| All 3 `z.any()` state tools attached, new adapter | **ACCEPTED**, no
400 |
## Not covered here
The showcase's aimock + Playwright e2e suite was **not** run locally
(this worktree has no installed toolchain). CI runs it on this PR;
please confirm the gen-ui / shared-state demos still pass before merge.
---
_Branch history shows an interim `z.string()` workaround that was
reverted once the upstream fix shipped; the net diff is the version bump
only. Squash-merge recommended._
Bump the canonical CopilotKit pin across all showcase integrations + shell
to 1.61.2 (canonical-pins.json, every package.json + package-lock.json),
which carries CopilotKit#5611: passing a catalog to the provider
(`<CopilotKit a2ui={{ catalog }}>`) now auto-enables A2UI and defaults tool
injection on, so the runtime no longer needs an explicit `a2ui` config.
Demonstrate the feature on the A2UI dynamic (declarative-gen-ui) demos by
removing the now-redundant runtime `a2ui` block (`injectA2UITool: true` +
`defaultCatalogId`) from:
- langgraph-python, langgraph-fastapi, langgraph-typescript
- strands, strands-typescript
- google-adk
The forwarded catalog supplies its own catalogId (sdk-js A2UI middleware
auto-derives `defaultCatalogId` from it), so the previous "Catalog not found"
fallback no longer applies.
Verified: validate-pins drift ratchet unchanged (38 / same hash);
langgraph-python D6 `gen-ui-declarative` green end-to-end (no Catalog-not-found).
The showcase authors A2UI catalog defs with root zod@4, but @a2ui/web_core's
GenericBinder schema scraper inspects Zod-3 internals (_def.typeName==='ZodUnion').
A zod@4 union reports _def.typeName===undefined → misclassified STATIC → the raw
{path} binding object reaches render → React error #31. Author this demo's catalog
with a zod-v3 (npm:zod@3.25.76) alias so the binder resolves bindings. Includes CR
hardening of the shared a2ui factory validation (plain-object data guard, unique-id
check, fail-loud on non-string secondary-LLM return).
@tanstack/ai-openai@0.15.6 (via @tanstack/openai-base@0.9.2) fixes the upstream
isStrictModeCompatible bug: it now detects typeless / z.any() properties and
sends strict:false, so OpenAI accepts them instead of 400ing with
"schema must have a type key".
With the upstream fix the showcase needs no schema change: revert the
z.string() workaround so the state tools use z.any() again (state-tools.ts is
now identical to main). Net change vs main is just the version bump:
@tanstack/ai 0.18.0 -> 0.35.0, @tanstack/ai-openai 0.9.1 -> 0.15.6.
Verified on the real OpenAI Responses API with all three z.any() state tools
attached (AGUISendStateSnapshot / AGUISendStateDelta / set_steps): adapter
emits strict:false, request ACCEPTED, model calls the tool.
@tanstack/ai-openai@0.15.5 dropped @tanstack/ai-client from its
peerDependencies (now just zod ^4 and @tanstack/ai ^0.35.0), and the
showcase never imported ai-client anyway. Bump to the latest releases and
remove the ai-client dep entirely.
Tool-schema serialization is unchanged: the z.string() state-tool params
still emit a strict-valid schema (root type:object, snapshot type:string,
strict:true), verified by capturing the outbound /responses tool payload on
0.35.0 / 0.15.5.
The built-in-agent showcase 400s on every prompt against the real OpenAI
Responses API. The state tools (AGUISendStateSnapshot / AGUISendStateDelta /
set_steps) declared their arbitrary payloads as z.any(), which serializes to
a typeless JSON Schema property. @tanstack/openai-base's
isStrictModeCompatible only screens for oneOf/allOf/not/$ref/$defs, so it
misses the missing type, sends the tool with strict:true, and OpenAI
rejects it:
400 Invalid schema for function 'AGUISendStateSnapshot':
In context=('properties','snapshot'), schema must have a 'type' key.
This is the real-OpenAI form of OSS-132 (the earlier zod3/zod4 drift that
produced a typeless root is already fixed by the zod 4 migration). It was
masked in production because the deployed showcase runs against aimock,
which replays fixtures without validating the request schema.
Fix: declare each arbitrary payload as a JSON-encoded string (z.string() ->
{ type: string }, strict-valid) and parse it in the server handler.
parseJson also tolerates an already-parsed object/array so recorded aimock
fixtures keep working. The TanStack->AG-UI converter reads the parsed
structure off the tool result, so it needs no change. Verified accepted and
correctly round-tripped by gpt-4o on the real Responses API with all three
tools attached.
Also bump @tanstack/ai 0.18.0 -> 0.34.0 and @tanstack/ai-openai 0.9.1 ->
0.15.4, and add the newly-required @tanstack/ai-client 0.18.2 peer. Adapter
schema output is byte-identical across the bump.
Bump every @copilotkit/* dependency across the showcase integrations and
the shell from 1.60.2 (and stray "latest" override pins) to an exact
1.61.1 pin, and move the canonical pin source of truth to match.
Regenerate each standalone npm package-lock.json with the same
--legacy-peer-deps flag the Dockerfiles use for "npm ci".
- showcase/integrations/*/package.json + package-lock.json
- showcase/integrations/langgraph-typescript/src/agent/*
- showcase/shell/package.json + package-lock.json
- showcase/scripts/showcase-canonical-pins.json: canonical 1.60.2 to 1.61.1
aimock stays on its own version line (1.26.1). The Python copilotkit SDK
was already 0.1.94 across every requirements.txt, so no change there.
validate-pins ratchet is unchanged (FAIL=38, identical hash);
validate-parity, validate-fixture-tool-surface, and the showcase/scripts
vitest suite (2102 tests) all pass.
Aligns dependency versions across all 19 showcase integrations to current
released minor versions for the 1.60.2 release cycle.
Package families:
- @copilotkit/{a2ui-renderer, react-core, react-ui, runtime, shared, sdk-js, voice}
1.59.4 -> 1.60.2 (18 integrations already staged; ms-agent-harness-dotnet
catches up from 1.57.2)
- @ag-ui/{client, core, encoder} 0.0.55 -> 0.0.57
- @ag-ui/mastra 0.2.1-beta.2 -> 0.2.4 (stable on 0.x; 1.0.x major held back)
Includes the previously-missed ms-agent-harness-dotnet integration in the
@copilotkit/* bump, plus the @copilotkit/web-inspector override pin.
Lockfile-only reconciliation via npm install --package-lock-only
--legacy-peer-deps (cmdk@0.2.1 pre-existing react^18 peer-dep is unaffected).
Bump canonicalCopilotKitVersion 1.59.2 -> 1.59.4 and pin every
integration's @copilotkit/* to 1.59.4 (locks regenerated). Keeps the
whole showcase on one version instead of letting the langgraph A2UI
demos deviate. Existing per-slug overrides (built-in-agent pkg.pr.new,
ms-agent-harness-dotnet 1.57.2) unchanged.
- New src/lib/header-forwarding.ts mirrors the cross-integration shim
pattern: reads inbound x-* headers and forwards them onto outbound
LLM calls so aimock fixture matching sees x-aimock-context
- Wire copilotkit route handler and tanstack factory through the shim
- Frontend-tools and a2ui-fixed-schema demo adjustments paired with the
conveyance work
- Bump package.json / package-lock.json against the updated tree
The "next" dist-tag was a workaround for Docker builds that can't resolve
workspace:* — but "next" has gone stale (1.55.2-next.1) while "latest" is
at 1.56.5. Renovate doesn't cover showcase/, so these never auto-bumped.
Switch all 19 showcase package.json files to "latest".
Install the pkg.pr.new build of @copilotkit/runtime from PR #4482 to
validate the runFinished fix that stops duplicate TOOL_CALL_END events
in convertTanStackStream after TanStack's RUN_FINISHED.
Adds hitl-in-app, readonly-state-agent-context, auth, agent-config,
voice, multimodal, byoc-hashbrown, and byoc-json-render demos to the
built-in-agent showcase. Each demo lives under src/app/demos/<feature>
with a dedicated runtime route under src/app/api/copilotkit-<feature>
where required.
Patterns:
- hitl-in-app and readonly-state-agent-context are frontend-only and
reuse the default /api/copilotkit runtime.
- auth uses createCopilotRuntimeHandler with the V2 onRequest hook to
reject requests missing the Bearer token.
- agent-config reads input.forwardedProps inside the BuiltInAgent
factory and prepends a tone/expertise/length-tuned system prompt.
- voice mounts at [[...slug]]/route.ts and registers
TranscriptionServiceOpenAI from @copilotkit/voice; falls back to a
401 with 'api key missing' when OPENAI_API_KEY is unset.
- multimodal reuses the base gpt-4o factory (vision-capable) and the
TanStack converter (convertInputToTanStackAI) for native image and
document part forwarding — no langgraph-specific binary rewrite.
- byoc-hashbrown and byoc-json-render add per-demo factories that pin
modelOptions.response_format to json_object so the streaming UI
parsers receive a single valid JSON object with no preamble.