Skip to content

Bug Report — Runtime APIs - concurrent env.AI.run() calls can return responses in the wrong format #8203

Open

Description

@nick-tsiones

What happened?

env.AI.run() stores its per-call options in a single shared #options field on the binding instance, and reads that field back after awaiting the upstream fetch to decide the response format. When two run() calls overlap on the same binding, the first call formats its response according to the second call's options.

In src/cloudflare/internal/ai-api.ts (line numbers as of 71cca5873, 2026-11-03):

  1. Each call writes its settings into the shared slot and then waits for the AI service to reply:

    • this.#options = options; — line 314
    • res = await this.#generateFetch(…); — line 337 (waits here)
  2. When the reply arrives, it reads the slot again to decide what format to return:

    if (this.#options.returnRawResponse || this.#options.websocket) {
      return res;
    }

    — line 357

  3. If another call started in the meantime, the slot now holds that other call's settings, so the earlier call returns its reply in the wrong format.

Example interleaving

const rawPromise = env.AI.run('some-model', inputs, { returnRawResponse: true });
// while the first call is still in flight, another call starts and finishes:
await env.AI.run('other-model', otherInputs);
const res = await rawPromise;
// `res` is a parsed object (or throws), not the Response the caller asked for

Observed consequences in both directions:

  • a returnRawResponse: true call overlapping with any plain run() call can receive a parsed object instead of a Response, breaking access to res.body (streaming);
  • a plain call overlapping with a returnRawResponse: true call can receive an unexpected Response.

Note that all other reads of this.#options (extra headers, prefix, signal, and the websocket branch at line 328) happen synchronously before the fetch is started, so the post-await read at line 357 is the only affected spot.

Expected behavior

Each run() call should format its own response according to the options passed to that call, regardless of concurrent calls on the same binding.

Reproduction

Deterministic reproduction requires two overlapping calls on the same binding instance: start a call whose upstream reply is delayed, start a quick returnRawResponse: true call while the first is in flight, then await the first — it resolves with the second call's format instead of its own.

I have a minimal one-line fix (read the call's own options at the format decision) plus regression tests for src/cloudflare/internal/test/ai/ ready and will follow up with a PR referencing this issue.

Environment

  • workerd main as of 71cca5873 (2026-11-03)
  • Reproduced by executing src/cloudflare/internal/ai-api.ts with concurrent run() calls against a stubbed fetcher; the bug is in the shared-slot logic itself, independent of runtime version.

Activity

  1. changed the title [-]🐛 Bug Report — Runtime APIs - concurrent env.AI.run() calls can return responses in the wrong format[/-] [+]Bug Report — Runtime APIs - concurrent env.AI.run() calls can return responses in the wrong format[/+] on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions