fix(ai): a ChatGPT turn that searched the web goes on after a tool call

The output items of a step were replayed as they came, web search calls
included. With store false the provider cannot resolve such a call again and
refused the next step with HTTP 400, which ended the turn with an error right
after a hike was proposed. The search calls are left out of the replay; what
they found is in the answer's text and its sources.
This commit is contained in:
Maurice
2026-10-23 22:16:22 +02:00
parent 53cefba3f3
commit 58204e72bd
2 changed files with 30 additions and 2 deletions
@@ -38,7 +38,8 @@ type ChatgptTokenSource = (rejected?: string) => Promise<string>;
* The provider-neutral history as Responses API input items. An assistant
* message this adapter wrote carries its output items, which are replayed as
* they came (reasoning with its encrypted content, the function calls with
* their namespace), since `store: false` keeps nothing on the provider's side.
* their namespace), since `store: false` keeps nothing on the provider's side;
* its web search calls are not, as the provider cannot resolve them again.
* One written by another adapter is rebuilt from its text and its calls.
*/
export function toResponsesInput(request: AiChatRequest): Record<string, unknown>[] {
@@ -70,7 +71,11 @@ function toItems(message: AiChatMessage): Record<string, unknown>[] {
if (message.role === 'tool') {
return [{ type: 'function_call_output', call_id: message.toolCallId, output: message.output }];
}
if (message.providerState?.adapter === 'openai-responses') return message.providerState.items;
// A web search the provider ran is left out: with nothing stored it cannot find the call again and
// refuses the whole request, and what the search found is in the answer's text and its sources.
if (message.providerState?.adapter === 'openai-responses') {
return message.providerState.items.filter((item) => item.type !== 'web_search_call');
}
return [
...(message.text
? [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: message.text }] }]
@@ -474,6 +474,29 @@ describe('OpenAiResponsesClient', () => {
]);
});
it('OARS-003b: leaves its own web search calls out of a replay, which the provider cannot resolve without storage', () => {
const search = {
type: 'web_search_call',
id: 'ws_1',
status: 'completed',
action: { type: 'search', query: 'Harz' },
};
const input = toResponsesInput({
...REQUEST,
context: undefined,
messages: [
{
role: 'assistant',
text: 'ignored',
toolCalls: [],
providerState: { adapter: 'openai-responses', items: [REASONING, search, CALL] },
},
{ role: 'tool', toolCallId: 'call_x', name: 'create_place', output: 'ok' },
],
});
expect(input).toEqual([REASONING, CALL, { type: 'function_call_output', call_id: 'call_x', output: 'ok' }]);
});
it('OARS-003: replays its own items verbatim and rebuilds another adapter turn from text and calls', () => {
const input = toResponsesInput({
...REQUEST,