Skip to content

F4b-Anthropic — native structured output via forced tool_use#107

Merged
fxspeiser merged 1 commit into
mainfrom
feature/f4b-anthropic-native-structured
Jun 7, 2026
Merged

F4b-Anthropic — native structured output via forced tool_use#107
fxspeiser merged 1 commit into
mainfrom
feature/f4b-anthropic-native-structured

Conversation

@fxspeiser

Copy link
Copy Markdown
Owner

Summary

Re-opened after #104 auto-closed when its base branch (#103/#106's Gemini branch) was deleted. Same diff as #104, rebased onto current main (which now has F4b OpenAI + F4b Gemini squashed in).

Third and final slice of the F4b family. When `SendArgs.jsonSchema` is set, the Anthropic adapter forces the model to emit its result through a `tool_use` block.

What lands

Build side

  • `AnthropicRequestBody` gains optional `tools[]` and `tool_choice`
  • New exported `ANTHROPIC_STRUCTURED_TOOL_NAME = "structured_output"`
  • `buildAnthropicRequest` accepts `opts.jsonSchema` — single-tool spec, `tool_choice` pins that tool, no schema translation (Anthropic accepts JSON Schema directly)

Parse side

  • `parseAnthropicResponse` distinguishes block types
  • `tool_use` blocks matching `ANTHROPIC_STRUCTURED_TOOL_NAME` → JSON-stringified `input` replaces concatenated text
  • Non-matching tool_use blocks (worker_tools' fetch/verify) flow through untouched
  • Plain text responses unchanged — baseline regression guard tested

Tests (+9)

  • build (4): tools + tool_choice attached / omitted / no schema translation / JSON-serializable
  • parse (5): tool_use input → text; tool_use trumps prose; only structured_output name captured; plain text baseline; array values stringify

Scoreboard

  • TS: 1,325 / 1,325
  • typecheck + build: clean

F4b coverage complete

Provider Mechanism
OpenAI `response_format: json_schema`
Gemini `responseSchema + responseMimeType`
Anthropic forced `tool_use`
Universal schema-in-prompt + tolerant parser (F4c)

🤖 Generated with Claude Code

Third and final slice of the F4b family (#102 OpenAI, #103 Gemini,
this PR Anthropic). When SendArgs.jsonSchema is set, the Anthropic
adapter forces the model to emit its result through a tool_use
block — closing the universal defense-in-depth coverage.

What lands

  src/providers/anthropic.ts:
    Request wiring (build):
      - AnthropicRequestBody gains optional tools[] and tool_choice
        fields. Tools is a single-element array; tool_choice pins
        the model to call exactly that tool.
      - NEW exported constant ANTHROPIC_STRUCTURED_TOOL_NAME =
        "structured_output". Same name on both sides of the wire so
        the parser can match deterministically.
      - buildAnthropicRequest accepts opts.jsonSchema. When set, the
        body grows a tools entry with the schema as input_schema and
        a tool_choice forcing that tool. Anthropic accepts JSON
        Schema directly, so unlike Gemini there is NO translation
        layer — the schema flows through verbatim.

    Response wiring (parse):
      - parseAnthropicResponse now distinguishes block types in the
        content array. tool_use blocks whose name matches
        ANTHROPIC_STRUCTURED_TOOL_NAME have their `input` field
        captured separately from text blocks.
      - When a matching tool_use block is present, its JSON-stringified
        input replaces the concatenated text payload. This keeps the
        downstream extractJson + validateSchema path entirely
        unaware of which transport carried the structured value —
        same parse → same validation → same envelope shape.
      - Non-matching tool_use blocks (e.g. worker_tools' fetch/verify
        in a different ReAct context) flow through untouched; only
        the structured_output tool name triggers the surface change.
      - Plain text responses keep working unchanged — baseline
        regression guard tested.

  src/providers/anthropic.ts (sendAnthropic):
    Threads args.jsonSchema into the builder.

Tests (+9)

  test/providers/anthropic-native-json.test.ts:
    build (4): tools + tool_choice attached on jsonSchema; omitted
      otherwise; no schema translation (rich JSON Schema flows
      verbatim into input_schema, including $schema and
      additionalProperties:false); JSON-serializable round trip.
    parse (5): tool_use input → JSON-stringified text; tool_use
      trumps prose text block (forced-tool semantics); only the
      structured_output tool name is captured (other tool names
      fall through); plain text still works; array values
      JSON-stringify correctly.

Scoreboard
  TS: 1,325 / 1,325 (+9 over #103's 1,316)
  typecheck + build: clean

F4b coverage complete

  OpenAI    response_format: {type: "json_schema", strict: true}     PR #102
  Gemini    generationConfig.{responseSchema, responseMimeType}      PR #103
  Anthropic tools[] + tool_choice: forced tool_use                   THIS PR
  Universal schema-in-prompt + tolerant parser (F4c)                 main

Every supported provider now has a native channel for structured
output. SendArgs.jsonSchema is the single switch the caller flips;
each adapter normalises to its own native shape and the universal
parse path catches any miss. Schema-in-prompt + F4c's tolerant
parser remain the safety net for providers we don't yet have native
mode wired for (xai/mistral/groq/deepseek under openai-compat).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fxspeiser fxspeiser merged commit 2799bd8 into main Jun 7, 2026
8 checks passed
@fxspeiser fxspeiser deleted the feature/f4b-anthropic-native-structured branch June 7, 2026 23:04
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