Skip to content

Support non-text reasoning parts and consider decoupling reasoning in the spec #13321

@felixarntz

Description

@felixarntz

Reasoning in LanguageModelV3 is text-only (text: string). Some providers produce non-text reasoning (images, files, audio). In theory even tool calls could be used in reasoning (as the model "thinks" about making tool calls). Either way, it probably makes sense to decouple reasoning from the requirement of being text in our spec.

Some alternative ideas:

Option 1: New Dedicated Types (e.g. reasoning-file)

Add new type discriminants prefixed with reasoning- for each non-text format.

// New union member in LanguageModelV4Content
{ type: 'reasoning-file'; mediaType: string; data: string | Uint8Array; }

// New stream part
{ type: 'reasoning-file'; mediaType: string; data: string | Uint8Array; }

Option 2: Reasoning Flag on Existing Types

Add reasoning?: boolean to existing content/stream types. Existing type: 'reasoning' is removed / deprecated.

// Existing types gain a flag
{ type: 'text'; text: string; reasoning?: boolean; }
{ type: 'file'; mediaType: string; data: string | Uint8Array; reasoning?: boolean; }

// Stream parts gain a flag
{ type: 'text-start'; id: string; reasoning?: boolean; }
{ type: 'file'; mediaType: string; data: ...; reasoning?: boolean; }

Alternatively, we could do something like this for type safety:

type WithReasoning<T> = T & { reasoning: true };
type ReasoningText = WithReasoning<{ type: 'text'; text: string }>;

Option 3: Separate reasoning Array

Store reasoning in a dedicated reasoning field using the same content types. Existing type: 'reasoning' is removed / deprecated.

type LanguageModelV4GenerateResult = {
  content: Array<LanguageModelV4Content>;
  reasoning: Array<LanguageModelV4Content>; // NEW
  // ...
};

Comparison

Option 1: New Types Option 2: Flag Option 3: Separate Array
Backward compat Non-breaking, purely additive union members Breaking: existing reasoning type/stream parts become redundant Breaking: all providers must return new reasoning field
Provider burden Highest: must emit distinct types for reasoning vs non-reasoning content Lowest: just set reasoning: true on existing types Medium: must split output into two arrays
Future extensibility Each new content type needs a reasoning-* variant Automatic: any content type can carry the flag Automatic: reasoning array accepts all content types
Type safety Best: single-field narrowing on type Good: two-field narrowing on type + reasoning Good: same content types, but can't rely on part data alone
Filtering reasoning Must match multiple type strings (reasoning, reasoning-file, ...) Single check: part.reasoning === true Field access: result.reasoning
Public result type alignment Matches current ContentPart union pattern (add { type: 'reasoning-file' } alongside { type: 'reasoning' }) Requires reworking ReasoningOutput and ContentPart since type: 'reasoning' would be replaced Closest match: public API already has a separate result.reasoning field
Input prompt changes Parallel new part types needed (ReasoningFilePart, ...) Flag on existing input parts, mirrors output Restructure assistant messages or use mixed approach

I'm unsure which option I prefer. Option 1 definitely feels easiest from a back compat / migration perspective, though that doesn't necessarily mean it's what we should go for. Curious what you think.

Metadata

Metadata

Assignees

Labels

ai/corecore functions like generateText, streamText, etc. Provider utils, and provider spec.ai/providerrelated to a provider package. Must be assigned together with at least one `provider/*` labelfeatureNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions