Skip to content

fix(output): send responseFormat only on calls that offer no tools - #660

Merged
LinuxDevil merged 1 commit into
mainfrom
fix/structured-output-tools-loop
Oct 16, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
fix/structured-output-tools-loop

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Problem

With output (a zod schema) and tools, every model call sent a strict json_schema responseFormat, including calls that offered tools. openai/gpt-4o-mini via OpenRouter then kept calling tools and never answered: a consumer harness hit maxSteps in 18 of 18 runs. That is the setup the docs lead with. When a captured sub-agent request was replayed against OpenRouter, it returned tool_calls 6/6 with response_format and stop with valid JSON 6/6 without it.

Fix

  • responseFormat is sent only on calls that offer no tools: an agent with no tools, the forced "answer now" call at maxSteps (toolChoice: 'none' counts as no tools), and the repair step, which is flagged on the run state. Calls that offer tools answer from the schema in the prompt, and the existing parse/repair still applies.
  • New escape hatch output: { schema, responseFormat: 'final' | 'always' }. The default is 'final', or 'always' with promptSchema: false, because the response format is then the only copy of the schema. An invalid value throws ConfigurationError.
  • object.delta streaming was keyed on request.responseFormat. It now depends on the run having output, so it still streams on tool-offering steps.
  • Docs: structured-output (when responseFormat is sent, how it interacts with tools, a table of modes), upgrading (behavior-change row with opt-out), durable-execution (agent.resume() resolves with a result rather than a stream, agent.stream([], { sessionId }) streams a resumed run, pending() also lists runs active in other processes), installation (TS 6+ needs "types": ["node"], since the root .d.mts uses Buffer and imports node:crypto/node:http/node:net). CHANGELOG, api reports, llms.txt.

Verification

  • New mock-provider tests in structuredOutput.test.ts: no responseFormat on tool steps, prose with embedded JSON parses with no repair, a plain-text answer gets repaired by a call that carries responseFormat, later steps go without it again, the forced answer and toolChoice: 'none' carry it, 'always' mode, the promptSchema: false default, invalid mode rejected, object.delta on a tool step.
  • npx tsc --noEmit, npm run lint, npm run build, api:check, docs:llms:check all clean. Full vitest: the only failures are Windows/env ones (create-lousho-agent template e2e, sqlite lock timing, http timeouts).
  • New live test src/execution/structuredOutputTools.live.test.ts (gpt-4o-mini, one tool, zod output) passed 3 of 3 runs, each in 2 steps with a valid object. A/B against real npm/GitHub tools with the harness's researcher prompt: 'always' (old behavior) and 'final' (new) both took 3 steps in this setup. The loop shows up in the captured-request replay above (6/6 vs 0/6), not in these fresh runs.

🤖 Generated with Claude Code

@LinuxDevil
LinuxDevil force-pushed the fix/structured-output-tools-loop branch 2 times, most recently from 14a1bb5 to ad1f2ab Compare October 16, 2026 20:49
A strict json_schema response format next to tools made
openai/gpt-4o-mini via OpenRouter call tools on every step until maxSteps.
responseFormat now goes only on calls without tools (no-tool agents, the
forced answer at maxSteps, the repair step); tool-offering calls answer
from the in-prompt schema with the existing parse/repair.
output: { schema, responseFormat: 'always' } restores the old behavior and
is the default with promptSchema: false. object.delta no longer depends on
responseFormat being set. Docs: structured output, durable execution
(resume() returns a result; pending() lists runs active elsewhere),
installation (TS 6+ needs "types": ["node"]).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil force-pushed the fix/structured-output-tools-loop branch from ad1f2ab to 7fc68c1 Compare October 16, 2026 20:51
@LinuxDevil
LinuxDevil merged commit c0d281a into main Oct 16, 2026
1 of 7 checks passed
@LinuxDevil
LinuxDevil deleted the fix/structured-output-tools-loop branch October 16, 2026 20:55
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