ID-71 Lane B — Structured outputs + citations API currency (2026-06-09)
Lane B research input for {71.1}. Question: what is the CURRENT state of Anthropic structured outputs and citations APIs, and what should Knowledge Hub’s grounding standards be? Baseline being re-verified: S195-era research (SDK 0.78.0, Claude 4.6 launch window) which established (a)
strict: trueas deferred hygiene (OPS-30/bl-50), (b) citations × structured outputs API incompatibility forcing the 3-passlib/ai/draft.tspipeline, (c) “schema enforcement ≠ semantic correctness”.Evidence labels: VERIFIED = read directly from official docs/changelog (fetched 2026-06-09) or from the repo. INFERRED = reasoned from verified facts, not explicitly documented.
1. Repo grounding — KH’s current state (all VERIFIED from source)
Section titled “1. Repo grounding — KH’s current state (all VERIFIED from source)”| Item | State |
|---|---|
| SDK | @anthropic-ai/sdk 0.96.0 (package.json:66) — well past the 0.78.0 S195 baseline |
Model tiers (lib/anthropic.ts:28-30) | analysis: claude-sonnet-4-5, drafting: claude-opus-4-6, quality: claude-haiku-4-5; getAIModel() default claude-sonnet-4-6 (line 18) |
lib/ai/draft.ts Pass 1 (Sonnet) | Already migrated to structured outputs — output_config.format with json_schema (questionAnalysisSchema), JSON.parse with silent fallback default |
lib/ai/draft.ts Pass 2 (Opus) | search_result content blocks with citations: {enabled: true} (lines 232-245, 364-377), no beta header — correct, search results are GA (see §3) |
lib/ai/quality-check.ts Pass 3 (Haiku) | Already migrated to structured outputs — output_config.format (line 142) |
draft.ts header comment | ”Citations and Structured Outputs are incompatible in the Claude API, so these MUST remain as separate API calls” — still accurate (see §4) |
lib/ai/classify.ts | Two forced-tool-use passes: Pass 1 return_classification (tool_choice {type:'tool'}, line 1358; model = getAIModel() → sonnet-4-6) and Pass 2 return_entity_validation (line 1039; claude-haiku-4-5). No strict: true; nested objects partially missing additionalProperties: false; uses type: ['string','null'] union types |
lib/ai/extract-questions.ts:404 | model: 'claude-haiku-3-5' with forced tool use — not a published model alias. Haiku 3.5’s real ID was claude-3-5-haiku-20241022, retired on the first-party API 2026-02-19. This call site is either dead or 404ing; also absent from lib/ai/pricing.ts. Side-finding — flag into ID-71 scope |
| bl-50 backlog note | Confirms scope: strict: true + recursive additionalProperties: false on classify tool schemas; absorbed into ID-71 (D5), implementation = Phase-0.4, gated on this Lane B re-verification |
Net: KH already adopted structured outputs in 2 of 3 drafting passes post-S195. The
unfinished items are the classify/extraction tool schemas (strict: true) and the
question of whether the 3-pass split can collapse.
2. Structured outputs — current state (VERIFIED)
Section titled “2. Structured outputs — current state (VERIFIED)”Source: Structured outputs (platform.claude.com, fetched 2026-06-09); API release notes.
- GA since 2026-01-29 on the Claude API (“no beta header required”). The old
structured-outputs-2025-11-13beta header still works during a transition period but is unnecessary. Public beta launched 2025-11-14. - Canonical parameter is
output_config.format({type: "json_schema", schema}). The top-leveloutput_formatparameter is deprecated API-wide (“has moved tooutput_config.format”). SDKmessages.parse()acceptsoutput_formatas a convenience and translates internally. - Two independent features, combinable in one request: JSON output format
(
output_config.format) and strict tool use (strict: trueper tool). Docs: “You can use these features independently or together in the same request… Claude can call tools with guaranteed-valid parameters AND return structured JSON responses.” strict: truesemantics: constrained decoding guarantees tool names and inputs conform to the schema.additionalProperties: falseis required on every object in a strict schema.- Supported models (live doc): Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Sonnet 4.5, Opus 4.5, Opus 4.1, Haiku 4.5 (plus Mythos Preview). This covers every model KH has configured. (Claude Fable 5, launched today 2026-06-09, is listed as supporting structured outputs in Anthropic’s bundled claude-api reference; the live doc page fetched did not yet show it — INFERRED supported, immaterial to KH.)
- Schema limitations (unchanged in kind from S195): no recursive schemas, no
numerical constraints (
minimum/maximum/multipleOf), no string length constraints,additionalPropertiesonlyfalse, arrayminItemsonly 0 or 1. Supported:enum,const,anyOf/allOf, internal$ref, stringformat(uuid, date-time, email, uri, …). Python/TS SDKs strip unsupported constraints and validate client-side. - Complexity limits (new since S195): max 20 strict tools per request, max 24 optional parameters and 16 union-typed parameters combined across all strict schemas in a request.
- Latency/caching: first request with a new schema pays grammar compilation
(max 180 s timeout); compiled grammars cached 24 h from last use. Cache is keyed
on schema structure + tool set +
output_config.format— changing only a tool’sname/descriptiondoes not invalidate. The injected format system prompt counts toward input tokens. - Works with: tool use, streaming, token counting, batch processing (50% discount).
- Failure modes:
stop_reason: "refusal"→ output may not match schema;stop_reason: "max_tokens"→ JSON may be truncated. Both must be handled. - Prefill is dead: last-assistant-turn prefills return 400 on the entire
4.6/4.7/4.8 family —
output_config.formatis the prescribed replacement (migration guide, VERIFIED). KH has no prefill usage (confirmed by absence inlib/ai), so this is a standards point, not a remediation.
3. Citations + search results — current state (VERIFIED)
Section titled “3. Citations + search results — current state (VERIFIED)”Source: Citations, Search results (fetched 2026-06-09); release notes.
- Citations: supported on all active models except Haiku 3; enabled per
document block via
citations: {enabled: true}(must be all-or-none across the request’s documents). Three document types (plain text → char indices, PDF → page numbers, custom content → block indices).cited_textis free of output-token charges and free of input-token charges when passed back in later turns. Streaming emitscitations_delta. ZDR-eligible. - Search result content blocks (
type: "search_result"withsource,title,citations: {enabled: true}): GA on the Claude API since 2025-08-08 — “The beta headersearch-results-2025-06-09is no longer required.” Available on all current models incl. Opus 4.6/4.7/4.8, Sonnet 4.5/4.6, Haiku 4.5. KH’s beta-header-free usage indraft.tson SDK 0.96.0 is the correct current form (and is empirically working in production). - Citations work with prompt caching (cache the document/search-result blocks via
cache_control), token counting, and batches. Note from the prompt-caching invalidation hierarchy: toggling citations invalidates the system+messages cache tiers but preserves the tools-tier cache.
4. The critical question: can citations + structured outputs combine now? — NO (VERIFIED)
Section titled “4. The critical question: can citations + structured outputs combine now? — NO (VERIFIED)”The live citations page (fetched 2026-06-09) carries an explicit warning, quoted verbatim:
“Citations and Structured Outputs are incompatible. Citations cannot be used together with Structured Outputs. If you enable citations on any user-provided document (Document blocks or RequestSearchResultBlock) and also include the
output_config.formatparameter (or the deprecatedoutput_formatparameter), the API will return a 400 error. This is because citations require interleaving citation blocks with text output, which is incompatible with the strict JSON schema constraints of structured outputs.”
- The API changelog (scanned back through 2024) contains no entry lifting this restriction. The S195 finding stands as of 2026-06-09.
- The incompatibility is architectural (citation blocks interleave with free text; a constrained-decoding grammar over the whole output cannot accommodate them), so it is unlikely to be lifted by a quiet flag flip — expect any future fix to be a first-class “cited structured outputs” feature, worth watching the changelog for.
- Nuance (INFERRED): the 400 trigger is specifically
output_config.format/output_formatco-occurring with citation-enabled blocks.strict: trueon tool definitions is not named, and the structured-outputs page does not mention citations at all. A request combining strict tools + citation-enabled search results (e.g. an agentic loop that searches with validated tool inputs and then writes cited prose) is plausibly legal — untested. If ID-71 ever needs this shape, run a 2-call empirical probe first. Not needed for any current KH pipeline.
Timeline (VERIFIED from release notes)
Section titled “Timeline (VERIFIED from release notes)”| Date | Event |
|---|---|
| 2025-01-23 | Citations launched |
| 2025-07-03 | Search result content blocks beta (search-results-2025-06-09) |
| 2025-08-08 | Search result blocks GA on Claude API + Vertex (header dropped) |
| 2025-11-14 | Structured outputs public beta (structured-outputs-2025-11-13; Sonnet 4.5 + Opus 4.1) |
| 2025-12-04 | Structured outputs extended to Haiku 4.5 |
| 2026-01-29 | Structured outputs GA (no header; output_format → output_config.format) |
| — | No entry to date changing citations × structured-outputs incompatibility |
5. Verdicts
Section titled “5. Verdicts”(i) bl-50 / OPS-30 (strict: true on classify schemas) — still valid, now cheaper than ever
Section titled “(i) bl-50 / OPS-30 (strict: true on classify schemas) — still valid, now cheaper than ever”Everything that made this “cheap hygiene” at S195 has improved: strict tool use is GA (no beta header), KH’s SDK 0.96.0 fully supports it, and the 24 h grammar cache amortises the one-off compile latency. Exact change shape:
lib/ai/classify.tsPass 1 toolreturn_classification(def ~line 1226) and Pass 2 toolreturn_entity_validation(def ~line 1000): addstrict: trueat the tool level; recursively addadditionalProperties: falseto every nested object (entities, relationships, temporal_references, validated_entities, the per-date-item objects — several currently haverequiredbut noadditionalProperties).- Pre-flight audit against the new strict limits (these did not exist in the
S195 analysis): ≤ 24 optional parameters and ≤ 16 union-typed parameters per
request. classify’s schemas use
type: ['string','null']unions — count them; if over the limit, convert optional-nullable fields torequired+ nullable (a required union still counts toward the 16) or trim the schema. This is the only genuine risk in the change; everything else is mechanical. - Schema-stability caveat (INFERRED): the grammar cache is keyed on schema bytes. If any enum in the classify schema is built dynamically from the DB taxonomy, each taxonomy change forces a fresh compile (fine — 24 h cache per variant) but avoid anything per-request-dynamic in the schema.
- Extend the same treatment to
lib/ai/extract-questions.tsTENDER_METADATA_TOOL— and fix its deadclaude-haiku-3-5model ID (→claude-haiku-4-5, plus apricing.tsentry) while in the file. - Python pipeline (
scripts/kb_pipeline/classify.py) is prompt-driven with no tool schema — unaffected, as the bl-50 note already records. - The S195 lesson is unchanged:
strict: trueguarantees structural validity (no inventedrel_typeenum values, no malformed JSON); it does not guarantee the model picked the right enum value. Keep the existing semantic checks.
(ii) The 3-pass drafting split — remains forced; do not merge
Section titled “(ii) The 3-pass drafting split — remains forced; do not merge”Pass 2 (Opus, citation-bearing drafting over search_result blocks) cannot carry
output_config.format — verified 400 (§4). Passes 1 and 3 already use structured
outputs and are on the right pattern. Even setting the 400 aside, a forced tool call
inside the citations request would replace the cited-text output rather than augment
it (citations attach to text blocks — INFERRED), so there is no merge shape available.
The draft.ts header comment is current and should stay. The only worthwhile change in
the drafting pipeline is hardening (§iii fallbacks), not architecture.
(iii) Proposed KH grounding standard (seed for {71.3} TECH + Phase 5 W4 ADR)
Section titled “(iii) Proposed KH grounding standard (seed for {71.3} TECH + Phase 5 W4 ADR)”Decision rule — for every AI touchpoint, pick exactly one of three shapes:
- Structured data, no source attribution needed →
output_config.formatwithjson_schema(GA). This is the default for analysis, quality checks, metadata extraction, summaries-as-data. Validate the parsed result with the existing zod schema (z.inferremains the canonical composed-type source per CLAUDE.md). - Model-decides / multi-tool or forced-tool extraction → tool use with
strict: true+additionalProperties: falseon every tool (constrained decoding at the token level). Forced single-tool extraction (tool_choice: {type:'tool'}) andoutput_config.formatare near-equivalent here; preferoutput_config.formatfor new single-shot extractions (simpler response handling), keep strict tools where the call site already uses the tool pattern (classify) or genuinely needs tools. - Output must be traceable to KB sources → citations over
search_resultcontent blocks (GA, no header). These calls return interleaved prose + citations and must not includeoutput_config.format/output_format.
Needs both structure and citations? → split into two calls (the 3-pass pattern is the canonical instance): cited-prose call first, structured call second (or in parallel where independent). Record this as the standing constraint with a changelog watch — revisit only if Anthropic ships cited structured outputs.
Mandatory hardening at every structured touchpoint:
- Handle
stop_reason: "refusal"(schema conformance not guaranteed) and"max_tokens"(truncated JSON) explicitly — don’t let them fall into generic parse-failure paths. - No silent fallback defaults:
draft.tsPass 1’s baretry { JSON.parse } catch {}predates structured outputs; withoutput_config.formata parse failure now signals refusal/truncation, so log + surface it (keep a fallback value, lose the silence). - Never use assistant prefills (400 on all 4.6+ models) or prompt-only “respond in JSON” forcing.
- Keep schemas static per call site (grammar-cache discipline); changing tool
name/descriptionis free, changing schema structure recompiles. - Schema enforcement ≠ semantic correctness — evals/spot-checks remain the quality gate (S195 lesson, re-affirmed).
Sources
Section titled “Sources”- Structured outputs — https://platform.claude.com/docs/en/build-with-claude/structured-outputs (fetched 2026-06-09)
- Citations — https://platform.claude.com/docs/en/build-with-claude/citations (fetched 2026-06-09; incompatibility warning quoted verbatim)
- Search results — https://platform.claude.com/docs/en/build-with-claude/search-results (fetched 2026-06-09)
- Claude Platform release notes — https://platform.claude.com/docs/en/release-notes/api (fetched 2026-06-09; entries 2025-01-23 → 2026-06-09)
- Anthropic claude-api reference skill (bundled, cache date 2026-05-26) — model catalogue, migration guide (prefill 400s,
output_configcanonical shape), prompt-caching invalidation hierarchy - Knowledge Hub repo @
id71-reviewworktree —package.json,lib/anthropic.ts,lib/ai/draft.ts,lib/ai/classify.ts,lib/ai/quality-check.ts,lib/ai/extract-questions.ts,lib/citations.ts,lib/ai/pricing.ts,docs/reference/product-backlog.json(bl-50)