Skip to content

Commit cc94a06

Browse files
committed
Propagate abort signals to bindings/fetches
Fix request cancellation by passing abort signals through outbound calls. Update ai-gateway-provider to forward options.abortSignal to fetch and binding.run and widen the binding.run type to accept an options object. Update tanstack-ai create-fetcher utilities to forward init.signal to binding.run (gateway and workers binding paths) and to build/run options consistently. Update workers-ai-provider chat, embedding, and image models to include abortSignal when calling binding.run so canceled requests are properly aborted. Update tests to reflect the optional options object shape and ensure extraHeaders handling remains correct. Adds a changeset documenting the patch.
1 parent b079f2d commit cc94a06

8 files changed

Lines changed: 72 additions & 22 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"ai-gateway-provider": patch
3+
"workers-ai-provider": patch
4+
"@cloudflare/tanstack-ai": patch
5+
---
6+
7+
Fix request cancellation by propagating `abortSignal` to outbound network calls.
8+
9+
**ai-gateway-provider**: Pass `abortSignal` to the `fetch` call (API path) and to `binding.run()` (binding path) so that cancelled requests are properly aborted.
10+
11+
**workers-ai-provider**: Pass `abortSignal` to `binding.run()` for chat, embedding, and image models, matching the existing behavior in transcription, speech, and reranking models.
12+
13+
**@cloudflare/tanstack-ai**: Pass `signal` through to `binding.run()` in both `createGatewayFetch` (AI Gateway binding path) and `createWorkersAiBindingFetch` (Workers AI binding path).

packages/ai-gateway-provider/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ export class AiGatewayChatLanguageModel implements LanguageModelV3 {
141141
...Object.fromEntries(headers.entries()),
142142
},
143143
}));
144-
resp = await this.config.binding.run(updatedBody);
144+
resp = await this.config.binding.run(updatedBody, {
145+
signal: options.abortSignal,
146+
});
145147
} else {
146148
headers.set("Content-Type", "application/json");
147149
headers.set("cf-aig-authorization", `Bearer ${this.config.apiKey}`);
@@ -151,6 +153,7 @@ export class AiGatewayChatLanguageModel implements LanguageModelV3 {
151153
body: JSON.stringify(body),
152154
headers: headers,
153155
method: "POST",
156+
signal: options.abortSignal,
154157
},
155158
);
156159
}
@@ -243,7 +246,7 @@ export type AiGatewayAPISettings = {
243246
};
244247
export type AiGatewayBindingSettings = {
245248
binding: {
246-
run(data: unknown): Promise<Response>;
249+
run(data: unknown, options?: { signal?: AbortSignal }): Promise<Response>;
247250
};
248251
options?: AiGatewayOptions;
249252
};

packages/tanstack-ai/src/utils/create-fetcher.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// ---------------------------------------------------------------------------
44

55
export interface CloudflareAiGateway {
6-
run(request: unknown): Promise<Response>;
6+
run(request: unknown, options?: { signal?: AbortSignal }): Promise<Response>;
77
}
88

99
export interface AiGatewayBindingConfig {
@@ -245,7 +245,9 @@ export function createGatewayFetch(
245245
}
246246

247247
if ("binding" in config) {
248-
return config.binding.run(request);
248+
return config.binding.run(request, {
249+
signal: init?.signal ?? undefined,
250+
});
249251
}
250252

251253
return fetch(
@@ -335,10 +337,14 @@ export function createWorkersAiBindingFetch(
335337
if (body.response_format) inputs.response_format = body.response_format;
336338
if (stream) inputs.stream = true;
337339

340+
const runOptions: Record<string, unknown> = {};
341+
if (options?.extraHeaders) runOptions.extraHeaders = options.extraHeaders;
342+
if (init?.signal) runOptions.signal = init.signal;
343+
338344
const result = await binding.run(
339345
model,
340346
inputs,
341-
options?.extraHeaders ? { extraHeaders: options.extraHeaders } : undefined,
347+
Object.keys(runOptions).length > 0 ? runOptions : undefined,
342348
);
343349

344350
if (stream && result instanceof ReadableStream) {

packages/tanstack-ai/test/binding-fetch.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,14 @@ describe("createWorkersAiBindingFetch", () => {
571571
});
572572

573573
expect(binding.run).toHaveBeenCalledOnce();
574-
const [, , options] = binding.run.mock.calls[0]!;
575-
expect(options).toBeUndefined();
574+
const [, , options] = binding.run.mock.calls[0]! as [
575+
unknown,
576+
unknown,
577+
Record<string, unknown> | undefined,
578+
];
579+
if (options) {
580+
expect(options).not.toHaveProperty("extraHeaders");
581+
}
576582
});
577583

578584
it("should pass response_format to binding for structured output", async () => {

packages/tanstack-ai/test/workers-ai-adapter.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ describe("WorkersAiTextAdapter config modes", () => {
998998

999999
expect(binding.run).toHaveBeenCalledOnce();
10001000
const [, , options] = binding.run.mock.calls[0]!;
1001-
expect(options).toEqual({
1001+
expect(options).toMatchObject({
10021002
extraHeaders: { "x-session-affinity": "my-session-id" },
10031003
});
10041004
});
@@ -1015,7 +1015,13 @@ describe("WorkersAiTextAdapter config modes", () => {
10151015
);
10161016

10171017
expect(binding.run).toHaveBeenCalledOnce();
1018-
const [, , options] = binding.run.mock.calls[0]!;
1019-
expect(options).toBeUndefined();
1018+
const [, , options] = binding.run.mock.calls[0]! as [
1019+
unknown,
1020+
unknown,
1021+
Record<string, unknown> | undefined,
1022+
];
1023+
if (options) {
1024+
expect(options).not.toHaveProperty("extraHeaders");
1025+
}
10201026
});
10211027
});

packages/workers-ai-provider/src/workersai-chat-language-model.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ export class WorkersAIChatLanguageModel implements LanguageModelV3 {
178178

179179
const output = await this.config.binding.run(
180180
args.model as keyof AiModels,
181-
// Content arrays for vision are valid at runtime but not in the
182-
// binding's strict TypeScript definitions (which expect string content).
183181
inputs as AiModels[keyof AiModels]["inputs"],
184-
runOptions,
182+
{
183+
...runOptions,
184+
signal: options.abortSignal,
185+
} as AiOptions,
185186
);
186187

187188
if (output instanceof ReadableStream) {
@@ -228,7 +229,10 @@ export class WorkersAIChatLanguageModel implements LanguageModelV3 {
228229
const response = await this.config.binding.run(
229230
args.model as keyof AiModels,
230231
inputs as AiModels[keyof AiModels]["inputs"],
231-
runOptions,
232+
{
233+
...runOptions,
234+
signal: options.abortSignal,
235+
} as AiOptions,
232236
);
233237

234238
// If the binding returned a stream, pipe it through the SSE mapper

packages/workers-ai-provider/src/workersai-embedding-model.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export class WorkersAIEmbeddingModel implements EmbeddingModelV3 {
5252
this.config = config;
5353
}
5454

55-
async doEmbed({ values }: EmbeddingModelV3CallOptions): Promise<EmbeddingModelV3Result> {
55+
async doEmbed({
56+
values,
57+
abortSignal,
58+
}: EmbeddingModelV3CallOptions): Promise<EmbeddingModelV3Result> {
5659
if (values.length > this.maxEmbeddingsPerCall) {
5760
throw new TooManyEmbeddingValuesForCallError({
5861
maxEmbeddingsPerCall: this.maxEmbeddingsPerCall,
@@ -76,8 +79,9 @@ export class WorkersAIEmbeddingModel implements EmbeddingModelV3 {
7679
},
7780
{
7881
gateway: this.config.gateway ?? gateway,
82+
signal: abortSignal,
7983
...passthroughOptions,
80-
},
84+
} as AiOptions,
8185
);
8286

8387
return {

packages/workers-ai-provider/src/workersai-image-model.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class WorkersAIImageModel implements ImageModelV3 {
3131
size,
3232
aspectRatio,
3333
seed,
34+
abortSignal,
3435
}: Parameters<ImageModelV3["doGenerate"]>[0]): Promise<
3536
Awaited<ReturnType<ImageModelV3["doGenerate"]>>
3637
> {
@@ -47,12 +48,19 @@ export class WorkersAIImageModel implements ImageModelV3 {
4748
}
4849

4950
const generateImage = async () => {
50-
const output = (await this.config.binding.run(this.modelId as keyof AiModels, {
51-
height,
52-
prompt: prompt ?? "",
53-
seed,
54-
width,
55-
})) as unknown;
51+
const output = (await this.config.binding.run(
52+
this.modelId as keyof AiModels,
53+
{
54+
height,
55+
prompt: prompt ?? "",
56+
seed,
57+
width,
58+
},
59+
{
60+
gateway: this.config.gateway,
61+
signal: abortSignal,
62+
} as AiOptions,
63+
)) as unknown;
5664

5765
return toUint8Array(output);
5866
};

0 commit comments

Comments
 (0)