Goal
Replace the hand-rolled JSON extraction scaffolding — <JSON_TAG> sentinels,
unwrapFencedJson, brace-matching, jsonrepair, the JsonExtractionError
taxonomy in src/utils/llmProxy.ts, and the TASK-167 prompt-contract text baked
into system prompts — with Anthropic native structured outputs
(output_config.format = { type: 'json_schema', schema }). This deletes the
entire "model returned unparseable JSON" failure class.
Verified plumbing (no proxy change required)
- Client
LlmProxyOptions.outputConfig already exists and is sent in the request
body (src/utils/llmProxy.ts).
- Proxy
normalizeOutputConfig is a pure passthrough; output_config is
forwarded to messages.create; the effort-strip only touches .effort. So
output_config.format flows through unmodified — confirmed against
proxy/facetServer.js.
- GA on every model Facet uses (Sonnet 4.6, Opus 4.x, Haiku 4.5). No beta header.
Plan (incremental, measured)
Known constraints / decisions
identity.extract is blocked on schema shape. Its output contract
(EXTRACTION_SYSTEM_PROMPT) contains open-keyed maps — metrics: {} (arbitrary
keys → string | number | boolean) and preferences.constraints: {}. Structured
outputs requires additionalProperties: false on every object and does not
support additionalProperties set to a schema, so these are unrepresentable
as-is. Decide a representation (typed map, [{ key, value }] array, or
stringify — each lossy or migration-touching) before migrating extraction. This
is why the pilot is bulletReframing, not identity.extract.
thinking + format is untested. Neither pilot call passes a thinking
budget, so the structured-outputs × adaptive-thinking interaction is not
exercised here. Test it explicitly on the first thinking-enabled call that
migrates before relying on it.
- Schema tooling for the rollout. The pilot hand-writes a tiny JSON Schema
(zero new deps, guaranteed constrained-decoding-compatible). For the broader
rollout, adopt Zod (z.toJSONSchema()) for ergonomics plus a runtime
validation backstop — a deliberate dependency/bundle decision, not a pilot
side-effect.
- Structured outputs guarantees shape, not content — keep the existing
semantic validation on every migrated call.
- Incompatible with citations and assistant prefill (neither is used on the
migrated calls).
Rollback
Per-call flag flip back to the sentinel path. The old extraction code stays until
the migration completes, so rollback is a config change, not a revert.
Acceptance criteria
Context: derived from the AI-proxy caching + structured-outputs handoff.
Prompt caching (the handoff's item 1) was assessed and deliberately not
pursued — at current prompt sizes only 2 of ~20 system prompts clear the model
cache minimums (Sonnet 4.6 = 2048, Opus/Haiku = 4096 tokens), and the
large stable context (identity) is re-serialized per call in the user message and
mutates across the bulk-deepen loop, so it would not cache. Revisit if prompts
grow or a hot loop with a frozen snapshot emerges.
Goal
Replace the hand-rolled JSON extraction scaffolding —
<JSON_TAG>sentinels,unwrapFencedJson, brace-matching,jsonrepair, theJsonExtractionErrortaxonomy in
src/utils/llmProxy.ts, and the TASK-167 prompt-contract text bakedinto system prompts — with Anthropic native structured outputs
(
output_config.format = { type: 'json_schema', schema }). This deletes theentire "model returned unparseable JSON" failure class.
Verified plumbing (no proxy change required)
LlmProxyOptions.outputConfigalready exists and is sent in the requestbody (
src/utils/llmProxy.ts).normalizeOutputConfigis a pure passthrough;output_configisforwarded to
messages.create; the effort-strip only touches.effort. Sooutput_config.formatflows through unmodified — confirmed againstproxy/facetServer.js.Plan (incremental, measured)
bulletReframing(first PR under this issue). Trivial{ reframed, reasoning }contract — proves the mechanism end-to-end anddeletes its
extractJsonBlock/JsonExtractionErrorpath on the flag. Gatedby
VITE_FACET_STRUCTURED_OUTPUTS(with a per-call override for tests).Measure:
JsonExtractionErrorrate → 0 on the flagged path.a per-call fallback until each is migrated and verified.
identity.extract— requires schema-shape decisions (blockerbelow).
<JSON_TAG>sentinel machinery,unwrapFencedJson,JsonExtractionError, thejsonrepairdependency, and theTASK-167 prompt-contract text in the affected system prompts.
Known constraints / decisions
identity.extractis blocked on schema shape. Its output contract(
EXTRACTION_SYSTEM_PROMPT) contains open-keyed maps —metrics: {}(arbitrarykeys → string | number | boolean) and
preferences.constraints: {}. Structuredoutputs requires
additionalProperties: falseon every object and does notsupport
additionalPropertiesset to a schema, so these are unrepresentableas-is. Decide a representation (typed map,
[{ key, value }]array, orstringify — each lossy or migration-touching) before migrating extraction. This
is why the pilot is
bulletReframing, notidentity.extract.thinking+formatis untested. Neither pilot call passes a thinkingbudget, so the structured-outputs × adaptive-thinking interaction is not
exercised here. Test it explicitly on the first thinking-enabled call that
migrates before relying on it.
(zero new deps, guaranteed constrained-decoding-compatible). For the broader
rollout, adopt Zod (
z.toJSONSchema()) for ergonomics plus a runtimevalidation backstop — a deliberate dependency/bundle decision, not a pilot
side-effect.
semantic validation on every migrated call.
migrated calls).
Rollback
Per-call flag flip back to the sentinel path. The old extraction code stays until
the migration completes, so rollback is a config change, not a revert.
Acceptance criteria
bulletReframingto native structured outputs;JsonExtractionErrorrate on the flagged path drops to zero.extractJsonBlock/JsonExtractionErrorpath.identity.extractschema-shape decision is documented and implemented (or explicitly deferred with rationale).<JSON_TAG>sentinel,unwrapFencedJson,JsonExtractionErrortaxonomy,jsonrepairdependency, and TASK-167 prompt-contract text are deleted when the last caller is migrated.Context: derived from the AI-proxy caching + structured-outputs handoff.
Prompt caching (the handoff's item 1) was assessed and deliberately not
pursued — at current prompt sizes only 2 of ~20 system prompts clear the model
cache minimums (Sonnet 4.6 = 2048, Opus/Haiku = 4096 tokens), and the
large stable context (identity) is re-serialized per call in the user message and
mutates across the bulk-deepen loop, so it would not cache. Revisit if prompts
grow or a hot loop with a frozen snapshot emerges.