Answer on Responses API models, and set how hard they think (#212) - #268
Open
ethanstoner wants to merge 2 commits into
Open
Answer on Responses API models, and set how hard they think (#212)#268ethanstoner wants to merge 2 commits into
ethanstoner wants to merge 2 commits into
Conversation
…it#212) Pointing BOT_MODEL at a gpt-5.6-* model gave a Bot that started, reported healthy on /health with responsesApi true, and then said nothing at all: RUN_STARTED, RUN_FINISHED, no text between them. The repository documents the symptom in two places, `.env.example` and the agent-langgraph service in `docker-compose.yml`, both noting it was driven against the real service. The cause is one line in the stream loop. It read the model's content as a string: const text = typeof chunk?.content === "string" ? chunk.content : ""; Chat completions streams a string, so that held for gpt-5.5. The Responses API does not: `@langchain/openai` converts every `response.output_text.delta` into a content block, and `convertResponsesDeltaToChatGenerationChunk` in its converters shows the shape, `[{ type: "text", text: delta, index }]`. So on 5.6 the condition was false for every delta, `text` was empty every time, `continue` ran every time, and the run ended having emitted no TEXT_MESSAGE_CONTENT at all. Both shapes are read now, in `deltas.ts` so it can be tested without binding a port, the reason `history.ts` is its own module too. Only blocks of type "text" are read. A reasoning model streams its summary in that same array under a different type, and it is the Bot's private working rather than its answer, so a surface printing it would be showing the person something never meant for them. BOT_REASONING_EFFORT is what the issue asked for: none, minimal, low, medium, high, xhigh or max, the list the installed API types carry. Sent as `reasoning: { effort }` rather than the `reasoningEffort` convenience field, which the integration deprecated in favour of merging into that object. Unset sends nothing and the model keeps its provider's default. Validated at startup rather than passed on, which is the other half of the request: a value the API does not have is dropped somewhere down the stack, and a Bot that starts and then thinks for as long as it likes is worse than one that refuses and says why. The same for the two ways the setting reaches an API with nowhere to put it, a provider that is not OpenAI and a model not on the Responses API. All three messages name the variable that would fix it. No new state, no new listener, nothing serialised. Three constants read at startup and one pure function in the stream loop, so a second replica behaves identically to the first. Verified by driving the real entrypoint against a stand-in for OpenAI's Responses API emitting the documented SSE events, so the whole path runs including the integration's own parsing: before: RUN_STARTED -> RUN_FINISHED, no deltas after: RUN_STARTED -> TEXT_MESSAGE_START -> 6x TEXT_MESSAGE_CONTENT -> TEXT_MESSAGE_END -> RUN_FINISHED, and the upstream request carried "reasoning":{"effort":"high"} gpt-5.5 on chat completions was driven the same way and still streams, so the older path is unchanged. The three refusals were each run and exit 1 with their message.
ethanstoner
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 27, 2026 00:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #212.
What this changes
Two things, and the first is why the second was worth having.
The framework Bot answers on 5.6-tier models. #212 asks to run
gpt-5.6-lunaon the Responses API Bot. Today that Bot starts, reports healthy, and says nothing —RUN_STARTED,RUN_FINISHED, no text. This repository already documents it, in.env.exampleand on theagent-langgraphservice indocker-compose.yml:It is one line in the stream loop:
Chat completions streams
contentas a string, so that holds on 5.5. The Responses API does not.@langchain/openaiturns everyresponse.output_text.deltainto a content block — its ownconvertResponsesDeltaToChatGenerationChunkbuilds[{ type: "text", text: delta, index }]. So on 5.6 the condition was false for every delta,textwas""every time,continueran every time, and the run finished having emitted noTEXT_MESSAGE_CONTENTat all. Both shapes are read now.Only
type: "text"blocks are read. A reasoning model streams its summary into the same array under a different type; that is the Bot's private working rather than its answer, and a surface printing it would show the person something never meant for them. There is a test for exactly that.BOT_REASONING_EFFORT, which is what the issue asked for:none,minimal,low,medium,high,xhigh,max— the list the installed API types carry. Sent asreasoning: { effort }, not thereasoningEffortconvenience field the integration deprecated in favour of merging into that object. Unset sends nothing and the model keeps its provider's default.Validated at startup rather than passed onward, which is the other half of the request. An effort the API does not have is dropped somewhere down the stack, and a Bot that starts, looks configured and thinks for as long as it likes is worse than one that refuses and says why. Same for the two ways this setting reaches an API with nowhere to put it — a provider that is not OpenAI, and a model not on the Responses API. All three messages name the variable that would fix it.
Where it runs
TEXT_MESSAGE_CONTENTevents that should have been there, and no new channel.Boundary and audit
callTool, and the run assertion are unchanged.BOT_PROVIDERand a missing model key already behave.Changelog
CHANGELOG.mdunderUnreleased.Proof
The real entrypoint, driven against a stand-in for OpenAI's Responses API emitting the documented SSE events, so the whole path runs — including the integration's own parsing. No key needed, and the upstream request is captured so it can be asserted on.
Before,
BOT_MODEL=gpt-5.6-luna:After, same command:
With
BOT_REASONING_EFFORT=high, the upstream request carried it:{"model":"gpt-5.6-luna","stream":true,"reasoning":{"effort":"high"}, ...}No regression on the older path.
BOT_MODEL=gpt-5.5against a chat-completions stand-in still reportsresponsesApi: falseand still streams all six deltas.The three refusals, each run, each exiting 1:
Suite:
agent-langgraphandagent-botpass (15 tests, 4 new). I could not get a number out of the fulltest:cirun locally, so I am not claiming one: with a pgvector:pg17 container and migrations applied it sat at near-zero CPU for tens of minutes without producing output, across three attempts. That looks like a Windows-host problem rather than anything in this diff — the change is three constants and one pure function inagent-langgraph, which is not in the workspace the DB suite covers. CI will be the real check.bun run typecheckclean.agent-langgraphhas notypecheckscript and is not in thedeployablesmatrix, so I rantsc --noEmitoversrc/andtests/by hand — clean. Happy to add it to that matrix in a separate PR if you want the coverage; it looked out of scope here.biome formatandbiome lintare clean on the five files this touches. (Checked against LF copies — a Windows checkout withcore.autocrlf=truefailsformat:checkon 442 of 444 files, mine included, which is a line-ending artifact rather than anything in the diff.)Notes
.env.examplenote, thedocker-compose.ymlservice comment, and aBOT_REASONING_EFFORTentry in both.agent-bot, the hand-written chat-completions Bot, is deliberately untouched. The issue asks that it stay on a compatible model, and it already refuses to start on one whose tools it cannot use.