Skip to content

Workers AI reasoning types: closed per-model unions (Design 1, stacked on #7437) - #2

Closed
KastanDay wants to merge 3 commits into
kastan/workers-ai-reasoning-typesfrom
kastan/workers-ai-reasoning-closed-unions
Closed

KastanDay wants to merge 3 commits into
kastan/workers-ai-reasoning-typesfrom
kastan/workers-ai-reasoning-closed-unions

Conversation

@KastanDay

@KastanDay KastanDay commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Stacked on cloudflare#7437 (base: kastan/workers-ai-reasoning-types). Merge this PR into that branch to adopt Design 1; leave it unmerged to keep cloudflare#7437's open-string hint types.

Why

cloudflare#7437 publishes reasoning efforts as AiReasoningEffortHint<...>: suggested values, but any string type-checks. Once shipped, that can never be narrowed again without breaking user code. This PR loosens the types the minimum amount instead.

What changes

  • Each model's effort type is a closed union: what every model accepted before ("low" | "medium" | "high", plus "minimal" for the Responses API) plus the efforts that model supports (from Workers AI ConfigAPI metadata).
  • Aliases are documented in the hover text, not typed. Legacy efforts a model neither supports nor maps get a hover note ("For compatibility, the types also accept high, but the model may ignore or reject it.").
  • The shared reasoning_effort and Responses reasoning types go back to main. AiReasoningEffortHint, ChatCompletionsReasoningEffort and ResponsesInputReasoning are removed.
Model main this PR
GLM-5.2, DeepSeek V4 flash/pro low medium high + max none
GLM-5.3-flash low medium high + max
GLM-5.3 (new) (absent) exactly max high low; enable_thinking?: true
Nemotron 3 (approved break) low medium high no reasoning_effort; own chat_template_kwargs
Kimi K2.6 low medium high + none
Qwen3.8 low medium high + xhigh
gpt-oss (chat), Gemma 4, GLM-4.7-flash, Kimi K2.7 Code low medium high unchanged (hover docs only)
gpt-oss (Responses) minimal low medium high unchanged (hover docs only)
Models without reasoning metadata low medium high unchanged

New models get exact types; published models only widen

The generator (cloudflare/ai/sdk!1782) compares with the latest published @cloudflare/workers-types@5.20260923.1:

  • New models are exact. GLM-5.3 accepts exactly "max" | "high" | "low", and enable_thinking?: true (its reasoning is mandatory). GLM-5.3's hover docs omit compatibility aliases, since its type rejects them. Adds Nemotron Speech Streaming (@cf/nvidia/nemotron-speech-streaming-en-0.6b) with its own schema.
  • Published models only widen (table below).
  • Approved break: Nemotron 3 (@cf/nvidia/nemotron-3-120b-a12b) publishes its own schema: no top-level reasoning_effort (the model never supported it), and chat_template_kwargs gains low_effort and force_nonempty_content. Inputs remain structurally assignable from main; only object literals passing reasoning_effort stop compiling.
  • Removed: @cf/runwayml/stable-diffusion-v1-5-img2img is no longer offered, so its AiModels entry is removed. It used the shared BaseAiTextToImage, so no export is removed.

Compatibility with main

  • No export removed. New exports: GLM-5.3 and Gemma's renamed class and input type (from [Workers AI] Update model reasoning types cloudflare/workerd#7437), Nemotron Speech Streaming, and Nemotron 3's own input types.
  • The only AiModels key removed is img2img (checked by diffing keyof AiModels).
  • Every model's inputs type is assignable from main's (checked for all shared models); outputs are unchanged.
  • Unsupported efforts, aliases and arbitrary string values are rejected, as on main.

Validation

  • bazel build //types; types/generated-snapshot/ is exactly bazel-bin/types/definitions/.
  • bazel test //types/...: 29/29 pass, including the rewritten types/test/types/ai-reasoning.ts (exact per-model unions, legacy values accepted for published models, exact types for new models, Nemotron's controls, img2img removed, unsupported/alias/arbitrary values rejected).
  • Generator change: cloudflare/ai/sdk!1782 (stacked on !1778).

… unions

Replace the open AiReasoningEffortHint types (any string type-checks) with
closed per-model unions: the efforts every model accepted before
("low" | "medium" | "high", plus "minimal" for Responses) and the model's
supported efforts from Workers AI ConfigAPI metadata. Types widen only by
what a model actually supports, and still accept every value that compiled
against main. Aliases are documented in hover text, not typed.

The shared reasoning_effort and Responses reasoning types return to their
main definitions; AiReasoningEffortHint, ChatCompletionsReasoningEffort and
ResponsesInputReasoning are removed. Snapshots are generated with bazel.
Regenerated from the Workers AI SDK type generator, which now compares with
the latest published @cloudflare/workers-types:

- New models get exact types. GLM-5.3 accepts exactly "max" | "high" | "low"
  and its mandatory reasoning cannot be disabled (enable_thinking?: true).
  Adds Nemotron Speech Streaming (@cf/nvidia/nemotron-speech-streaming-en-0.6b).
- Nemotron 3 (@cf/nvidia/nemotron-3-120b-a12b) publishes its own schema: no
  top-level reasoning_effort (the model never supported it), and
  chat_template_kwargs gains low_effort and force_nonempty_content. This is
  a one-time approved breaking change for object literals passing
  reasoning_effort.
- Removes @cf/runwayml/stable-diffusion-v1-5-img2img, which is no longer
  offered.
- Published models keep their closed, non-narrowing effort unions.
GLM-5.3's exact type rejects its compatibility aliases, so its hover docs no
longer list them. Regenerated from cloudflare/ai/sdk!1782.
@KastanDay

Copy link
Copy Markdown
Owner Author

Closing: we're going with exact per-model reasoning types for every model instead, now in cloudflare#7437 (2501936).

@KastanDay KastanDay closed this Sep 23, 2026
KastanDay pushed a commit that referenced this pull request Oct 16, 2026
The TypeScript constructors read a source's, sink's or transformer's
members in their own order, checking type first, where WebIDL
converts the whole dictionary before the constructor checks type.
Pin both implementations as they are, ahead of the change to WebIDL
conversion:

- every member's read, through getters, on a default and a byte
  source, a sink and a transformer;
- which error wins when a bad callback meets an invalid type, an
  invalid highWaterMark or a byte stream's size function;
- autoAllocateChunkSize and expectedLength on a default source,
  which TypeScript does not read and C++ converts;
- autoAllocateChunkSize on a byte source: C++ converts an int, so a
  fraction truncates and 2 ** 31 and up throw; TypeScript requires a
  positive integer and sets no upper bound.

The ledgers record each divergence: readable #2, cloudflare#32 and cloudflare#34;
readable-byte #2, cloudflare#39 and cloudflare#40; writable cloudflare#3 and cloudflare#18; transform cloudflare#24
and cloudflare#27.
KastanDay pushed a commit that referenced this pull request Oct 16, 2026
Decision D-10, which fixes finding F-3. A byte source's
autoAllocateChunkSize had to be a positive integer, so a fraction
threw, and no upper bound applied, so a value above 2^53 - 1 failed
only when a read allocated it. The member now converts as WebIDL
converts an [EnforceRange] unsigned long long, and the byte
controller then rejects zero, as the spec's setup step does. Under
the TypeScript streams:

- A fraction truncates (1.5 gives 1-byte views), as under C++.
- 2^53 and up throw TypeError at construction. C++ throws from
  2^31, the end of its int.
- Zero throws 'autoAllocateChunkSize cannot be zero'; every other
  value the conversion refuses names the range.

The conversion lives in common.ts, beside normalizeExpectedLength.
The readable-byte ledger #2 pin flips its TypeScript side, and
fractionalAutoAllocateChunkSize is now parity.
KastanDay pushed a commit that referenced this pull request Oct 16, 2026
Decision D-10, which also closes the validation half of finding F-1.
The ReadableStream constructor checked the source's type, then a
byte stream's size function and the strategy's highWaterMark, and
only then did a controller read the callbacks; a default source's
autoAllocateChunkSize and expectedLength were not read at all.
WebIDL converts the whole UnderlyingSource dictionary, in
lexicographic order, type last as an enum, before the constructor
checks anything. common.ts now converts it that way, into a
null-prototype record (convertUnderlyingSource), and the controllers
take the record and the source, which stays the hooks' receiver.
Under the TypeScript streams:

- A non-callable pull, or a throwing pull getter, now beats an
  invalid type, as under C++.
- A non-callable pull now beats an invalid highWaterMark and a byte
  stream's size function (RangeError until now) and a zero
  autoAllocateChunkSize (TypeError 'cannot be zero'), as under C++.
- A default source converts autoAllocateChunkSize and expectedLength
  as a byte source does, with no zero check, although it uses
  neither. For autoAllocateChunkSize, -1, 'abc', NaN and Infinity
  now throw TypeError, where C++, which converts an int, accepts
  them; for expectedLength, 'abc' and -1 throw TypeError, as under
  C++.

The members are read in lexicographic order, as the spec reads them;
C++ reads them in its own order. Readable ledger #2, cloudflare#32 and cloudflare#34 and
readable-byte ledger cloudflare#39 and cloudflare#40 flip their TypeScript side; the new
cases of cloudflare#32 and cloudflare#40 are now parity, and cloudflare#40 gains a zero
autoAllocateChunkSize against a non-callable pull. The webstreams
AGENTS.md file map lists the three converters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant