fix(xai): fence cancelled x_search cache writes (#123996)

Prevent a completed standalone x_search request from building or caching a result after caller cancellation. Preserve the exact abort reason and force identical retries back through the provider request path.\n\nFixes #123964
This commit is contained in:
Peter Steinberger
2026-08-14 21:10:10 -07:00
committed by GitHub
parent 4e64649191
commit e26bae5b2a
2 changed files with 24 additions and 0 deletions
+23
View File
@@ -276,6 +276,29 @@ describe("xai x_search tool", () => {
expect(mockFetch).not.toHaveBeenCalled();
});
it("does not cache an X search result completed after caller cancellation", async () => {
const controller = new AbortController();
const reason = new Error("operator cancelled X search after response");
const mockFetch = vi
.fn()
.mockImplementationOnce(async () => {
controller.abort(reason);
return jsonResponse({ output_text: "Cancelled X answer", citations: [] });
})
.mockResolvedValueOnce(jsonResponse({ output_text: "Recovered X answer", citations: [] }));
global.fetch = withFetchPreconnect(mockFetch);
const tool = createConfiguredXSearchTool();
const query = "unique standalone x_search late-cancel cache regression";
await expect(tool.execute("xai-late-cancel", { query }, controller.signal)).rejects.toBe(
reason,
);
const recovered = await tool.execute("xai-late-cancel-retry", { query });
expect(mockFetch).toHaveBeenCalledTimes(2);
expect((recovered.details as { content?: string }).content).toContain("Recovered X answer");
});
it("uses the xAI Responses x_search tool with structured filters", async () => {
const mockFetch = installXSearchFetch();
const tool = createConfiguredXSearchTool({ xSearch: { maxTurns: 2 } });
+1
View File
@@ -242,6 +242,7 @@ export function createXSearchTool(options?: {
options: xSearchOptions,
...(signal ? { signal } : {}),
});
signal?.throwIfAborted();
const payload = buildXaiXSearchPayload({
query,
model,