Skip to content
LaravelBlog

Fix: structured output format no longer dropped when provider options set output_config

LaravelBlogBot
LaravelBlogBot

Contributed by kachelle

Part of the ai v1.2.0 release

Fix: structured output format no longer dropped when provider options set output_config

Pull request #1105 fixes a quiet failure mode at the intersection of two features: native structured output and provider options. When an agent returned output_config from providerOptions() on Anthropic — or text on OpenAI's Responses API — that option replaced the entire request section the gateway builds for structured output, taking the format payload (and your JSON schema) with it. The model, never seeing a schema, answered in free text. Both keys are now merged one level deep, so tuning like effort or verbosity lands alongside the schema instead of on top of it. Fixes #1062.

What broke

When you use structured output, laravel/ai writes part of the provider request for you. On Anthropic it builds output_config.format with a json_schema type; on OpenAI Responses it builds text.format. Provider options are applied afterwards with a plain top-level merge, which meant any provider option named output_config or text didn't add to that structure — it replaced it.

// providerOptions(): ['output_config' => ['effort' => 'low']]
// before: "output_config": {"effort": "low"}

The format subtree simply vanished. Nothing about the request failed, which made this an unpleasant bug to diagnose: the API returned a successful response, the model produced confident prose, and your parsing or validation layer was left holding strings where it expected arrays. It looked like the model was bad at JSON rather than like a request-building problem.

The fix: merge the nested key one level deep

Both gateways now check for the collision before merging. From the Anthropic request builder:

if (isset($body['output_config'], $providerOptions['output_config'])) {
    $providerOptions['output_config'] = array_merge(
        $body['output_config'],
        $providerOptions['output_config']
    );
}

The OpenAI Responses builder gets the same guard for text. The outgoing request now carries both halves:

{
    "output_config": {
        "format": {"type": "json_schema", "schema": {"...": "..."}},
        "effort": "low"
    }
}

A concrete setup that used to break and now works as intended:

public function providerOptions(Lab|string $provider): array
{
    return [
        Lab::Anthropic => [
            'thinking' => ['type' => 'enabled', 'budget_tokens' => 10000],
            'output_config' => ['effort' => 'low'],
        ],
        Lab::OpenAI => [
            'reasoning' => ['effort' => 'low'],
            'text' => ['verbosity' => 'low'],
        ],
    ];
}

Precedence still behaves the way you expect

The merge order is gateway body first, your options second, so the rules are predictable:

  • A format key you pass yourself still wins — you can replace or extend the schema-bearing section when you genuinely need to.
  • The merge is one level deep. Keys inside format are not individually merged; if you pass a format, yours is used wholesale.
  • Every other provider option (thinking, reasoning, top_p, and friends) still overrides exactly as before.

The PR locks this down with feature tests on both providers. The Anthropic test runs a structured agent with thinking and effort: 'low' and asserts that output_config.format.type === 'json_schema', output_config.effort === 'low', and thinking.type === 'enabled' all reach the request. The OpenAI equivalent asserts text.format.type, text.verbosity, and reasoning.effort coexist in the same payload.

Upgrade impact

Upgrading from v1.1.0 requires no code or config changes — this is a straight behavioral fix.

If you hit #1062, the change is welcome news: structured agents that were drifting into prose will start returning schema-conformant JSON again. Expect parsed responses to become arrays and objects where they were strings before. If that surprises you on upgrade, it's the fix working, not a regression.

If you deliberately set output_config or text and relied on the old full-replacement behavior — say, to intentionally drop the schema — that escape hatch still exists: pass your own format key and it wins. What you can no longer do is drop the schema accidentally by setting an unrelated key like effort.

If you never set these particular provider option keys, nothing changes for you.

Takeaways

  • Provider options for output_config (Anthropic) and text (OpenAI Responses) now merge one level deep instead of replacing the gateway-built section.
  • Structured output survives tuning like effort or verbosity — your schema reaches the model every time.
  • A format you pass explicitly still takes precedence, and all other provider options keep their existing override behavior.
  • Fixes #1062; no upgrade steps required beyond pulling v1.1.1+.

Sources

More from this release

Related Articles