Description
When using the OpenAI Responses API through AsIChatClient() in a multi-turn console app with function/tool invocation, requests intermittently fail with a 400 from the service:
Previous response with id 'resp_...' not found.
("code": "previous_response_not_found")
The failure is intermittent — the same conversation can run for several turns, then a turn fails. Once it fails, retrying the same user turn usually succeeds, which suggests a stale/never-persisted response id is being sent as previous_response_id rather than a permanently corrupt conversation state.
Microsoft.Extensions.AI.OpenAI is what maps ConversationId onto previous_response_id for the Responses client, so filing here rather than against openai/openai-dotnet.
Versions
| Package |
Version |
Microsoft.Extensions.AI |
10.9.0 |
Microsoft.Extensions.AI.OpenAI |
10.9.0 |
OpenAI |
2.12.0 |
ModelContextProtocol |
2.2.0 |
.NET 10, Windows. Endpoint is Azure OpenAI's OpenAI-compatible /openai/v1/ surface with BearerTokenPolicy.
Repro shape
var clientOptions = new OpenAIClientOptions
{
Endpoint = new Uri($"{azureOpenAIEndpoint.TrimEnd('/')}/openai/v1/"),
NetworkTimeout = TimeSpan.FromMinutes(5)
};
var openAIClient = new OpenAIClient(tokenPolicy, clientOptions);
var innerClient = openAIClient.GetResponsesClient().AsIChatClient();
var chatClient = new ChatClientBuilder(innerClient)
.UseFunctionInvocation()
.Build();
var chatOptions = new ChatOptions
{
ModelId = model,
AllowMultipleToolCalls = true,
ToolMode = ChatToolMode.Auto,
Tools = [.. mcpTools] // tools obtained from MCP servers
};
IList<ChatMessage> chatHistory = [new ChatMessage(ChatRole.System, systemPrompt)];
// per user turn:
chatHistory.Add(new ChatMessage(ChatRole.User, userMessage));
List<ChatResponseUpdate> receivedUpdates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(chatHistory, chatOptions))
{
receivedUpdates.Add(update);
// ... render text / reasoning / function call + result content ...
}
chatHistory.AddMessages(receivedUpdates);
The app keeps a single IList<ChatMessage> history and a single shared ChatOptions instance across turns, appends streamed updates back into history via AddMessages, and never sets ChatOptions.ConversationId itself. Tools are MCP tools, so most turns include at least one function call round trip inside UseFunctionInvocation.
What I ruled out
The original app also had .UseDistributedCache(...) and .UseChatReducer(new SummarizingChatReducer(...)) in the pipeline, so my first assumption was that one of those was replaying or rewriting history in a way that resurrected a stale conversation id. That turned out not to be it:
- Reproduces with and without
UseDistributedCache.
- Reproduces with and without
UseChatReducer / SummarizingChatReducer.
- Reproduces with local (stdio) MCP servers and with remote (HTTP) MCP servers, so it isn't specific to one transport or tool set.
With all of the optional middleware removed the failure still occurs, which is why I believe it lives in the Responses IChatClient response-chaining path (or in how ConversationId is round-tripped through streamed updates and AddMessages) rather than in application middleware.
Expected
Either:
- A conversation id that the service no longer recognizes should not be sent as
previous_response_id — the client should fall back to sending the full input history instead of a dangling chain; or
- If chaining is expected to be the caller's responsibility here, it would help a lot to have documented guidance on when
ConversationId is safe to carry across turns when UseFunctionInvocation performs intermediate round trips, and how a consumer is meant to detect and recover from previous_response_not_found.
Actual
An intermittent 400 previous_response_not_found surfaces to the caller as a ClientResultException, terminating the turn.
Notes
Happy to instrument and capture request payloads (previous_response_id vs. submitted input items) for the failing turn if that would help pin it down — let me know what would be most useful.
Description
When using the OpenAI Responses API through
AsIChatClient()in a multi-turn console app with function/tool invocation, requests intermittently fail with a 400 from the service:(
"code": "previous_response_not_found")The failure is intermittent — the same conversation can run for several turns, then a turn fails. Once it fails, retrying the same user turn usually succeeds, which suggests a stale/never-persisted response id is being sent as
previous_response_idrather than a permanently corrupt conversation state.Microsoft.Extensions.AI.OpenAIis what mapsConversationIdontoprevious_response_idfor the Responses client, so filing here rather than againstopenai/openai-dotnet.Versions
Microsoft.Extensions.AIMicrosoft.Extensions.AI.OpenAIOpenAIModelContextProtocol.NET 10, Windows. Endpoint is Azure OpenAI's OpenAI-compatible
/openai/v1/surface withBearerTokenPolicy.Repro shape
The app keeps a single
IList<ChatMessage>history and a single sharedChatOptionsinstance across turns, appends streamed updates back into history viaAddMessages, and never setsChatOptions.ConversationIditself. Tools are MCP tools, so most turns include at least one function call round trip insideUseFunctionInvocation.What I ruled out
The original app also had
.UseDistributedCache(...)and.UseChatReducer(new SummarizingChatReducer(...))in the pipeline, so my first assumption was that one of those was replaying or rewriting history in a way that resurrected a stale conversation id. That turned out not to be it:UseDistributedCache.UseChatReducer/SummarizingChatReducer.With all of the optional middleware removed the failure still occurs, which is why I believe it lives in the Responses
IChatClientresponse-chaining path (or in howConversationIdis round-tripped through streamed updates andAddMessages) rather than in application middleware.Expected
Either:
previous_response_id— the client should fall back to sending the full input history instead of a dangling chain; orConversationIdis safe to carry across turns whenUseFunctionInvocationperforms intermediate round trips, and how a consumer is meant to detect and recover fromprevious_response_not_found.Actual
An intermittent 400
previous_response_not_foundsurfaces to the caller as aClientResultException, terminating the turn.Notes
Happy to instrument and capture request payloads (
previous_response_idvs. submitted input items) for the failing turn if that would help pin it down — let me know what would be most useful.