Skip to main content
This guide covers breaking changes in @outputai/llm. The package now uses AI SDK 7 and its matching provider majors. Generation APIs drop native AI SDK call arguments. Skills load only from prompt frontmatter. Call-argument tools merge with prompt YAML tools. Prompt file config is a strict key list. Loaded messages carry resolved providerOptions instead of tag attributes. LLM traces use a single loaded prompt on start, keep raw usage and merged sources on end, and store normalized usage and cost as attributes.

Upgrade AI SDK and provider packages

@outputai/llm v0.12 requires these dependency majors:
  • ai: 7.x
  • @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/azure, and @ai-sdk/perplexity: 4.x
  • @ai-sdk/amazon-bedrock and @ai-sdk/google-vertex: 5.x
Update only the provider packages your application installs. The complete upstream changes are in the AI SDK 7 migration guide. The separate @perplexity-ai/ai-sdk search-tool package is not the official @ai-sdk/perplexity provider. Its latest release, 0.1.3, declares support for AI SDK 5 and 6 only. Projects using perplexitySearch() should replace it or verify its behavior with AI SDK 7 before upgrading; package managers may report an unmet peer dependency.

Rename stream callbacks and properties

streamText() and Agent.stream() now use onEnd instead of onFinish:
Rename the corresponding exported types:
  • WrappedStreamTextOnFinishEvent -> WrappedStreamTextOnEndEvent
  • WrappedStreamTextOnFinishCallback -> WrappedStreamTextOnEndCallback
The wrapped event continues to include result, cost, and merged sources. AI SDK 7 also renamed the full event stream:
fullStream remains available as a deprecated alias, but migrate to stream. textStream is unchanged. Direct AI SDK onStepFinish usage becomes onStepEnd. onChunk now receives every AI SDK 7 stream part, including start, finish, error, step boundaries, text boundaries, and reasoning boundaries. Update exhaustive handlers to ignore part types they do not use.

Review native AI SDK response usage

Output returns proxied AI SDK 7 results. If your application reads native response fields beyond Output’s result, cost, and merged sources, follow the official AI SDK 7 migration guide linked above instead of treating this guide as a replacement for it. Output’s merged sources now includes native sources accumulated across every AI SDK step as well as URLs extracted from tools.

Normalized usage and cost metadata

Cost calculation uses AI SDK 7’s detailed token counts when they reconcile with the aggregate input or output count. Incomplete breakdowns use the aggregate count. Missing cache pricing uses the regular input price, and missing reasoning pricing uses the regular output price. Traces now store separate llm:generation:usage and llm:generation:cost attributes. Both include providerId, modelId, nullable aggregate input, output, and total fields, plus detailed items. Priced calls also retain the deprecated llm:usage attribute for existing trace consumers. Usage items have an amount, a group (input or output), and one of these labels:
  • Input: no_cache, cache_read, or cache_write
  • Output: text or reasoning
When detailed counts do not reconcile with an aggregate count, that group uses label: null. LLMGenerationUsage.status is complete when both input and output usage are available; otherwise it is incomplete. Cost items repeat the usage identity and amount, then add ppm, item total, and a status:
  • ok: the matching price was available.
  • fallback: cache or reasoning usage used the regular input or output price.
  • missing: no applicable price was available; ppm and item total are null.
LLMGenerationCost.status is precise, imprecise, or incomplete based on those item statuses and usage completeness. Missing model or dimension pricing therefore produces a non-null, incomplete LLMGenerationCost whose total may be null. The complete cost object is null only when the pricing catalog itself could not be loaded. The new llm:generation:metering event is emitted whenever raw AI SDK usage can be normalized. Its payload is { usage: LLMGenerationUsage, cost: LLMGenerationCost | null }. Prefer it for new integrations because it preserves reported usage independently from pricing and records fallback and missing prices explicitly. The existing cost:llm:request event remains supported with its legacy payload shape, so existing handlers do not need a structural migration for v0.12. Its deprecated LLMUsageEvent retains the input, input_cached, output, and reasoning line types. Values can differ where AI SDK 7 reports a different breakdown or where v0.12 corrects legacy reasoning double-counting. Raw AI SDK usage remains available on the response and trace output.

Update Agent assumptions

Agent no longer inherits from AI SDK’s ToolLoopAgent. Its Output APIs retain their existing behavior, but code must not depend on Agent instanceof ToolLoopAgent or other inherited ToolLoopAgent members. The intended MessageStore conversation behavior is unchanged. Output reads AI SDK 7’s accumulated responseMessages so intermediate tool-call and tool-result messages continue to be persisted once. Custom stores must preserve all AI SDK 7 ModelMessage content parts. File data may require binary-safe serialization, and assistant content can contain the new reasoning-file part.

Review provider behavior

  • OpenAI reasoning requests now return detailed reasoning summaries by default when reasoning is enabled. Set providerOptions.openai.reasoningSummary to null in prompt frontmatter to disable them.
  • Function-valued tool descriptions are now accepted in addition to string descriptions.
Call-level toolApproval, runtimeContext, and toolsContext are not exposed by the v0.12 Output wrapper. AI SDK 7’s deprecated tool-level needsApproval field remains accepted.

Skills load only from the prompt file

generateText, streamText, generateTextWithStreaming, and Agent no longer accept a skills argument. Dynamic skill resolvers (sync or async functions) are gone with it. Passing skills throws:
skill() is no longer exported. Colocated skills/ auto-discovery is gone: a skills/ folder next to the prompt is not loaded unless you list it in frontmatter.

Move call-argument and inline skills into the prompt

Before

After

Put the instructions in a markdown file and list the path in frontmatter. Paths are relative to the prompt file.
prompts/skills/audience.md
prompts/writer@v1.prompt
A directory path loads every .md file under it (recursive):
A single string is still valid YAML and is coerced to an array at load time:

Restore colocated skills that used auto-discovery

Before

No skills: key in the prompt. Output discovered ./skills automatically.

After

Keep the folder. Add an explicit path:

Prompt tools and call-argument tools merge

Call-argument tools no longer replace the whole prompt YAML tools map.
  • Prompt YAML tools and call-argument tools are merged.
  • The same key: the caller wins.
  • When skills are present, load_skill is added last and cannot be overridden.

Before

googleSearch was dropped. Only lookup was sent.

After

Both are sent: { googleSearch, lookup }. If you meant to disable YAML tools, remove them from the prompt (or override that key on the call).

Loaded prompt shape

loadPrompt returns the parsed prompt object.

Before

After

The SkillsArg and Skill types are removed. Skills are now internal loaded values; declare file paths in prompt frontmatter instead of constructing or typing skill objects.

Per-message options resolve at load

v0.11 kept the role tag’s attributes on the loaded message and compiled options="..." into AI SDK providerOptions at generate time. v0.12 compiles that at loadPrompt. LLM traces (input.prompt.messages) use the same loaded shape.

Before

After

The only supported role-tag attribute is options. Any other attribute throws at load (previously this could fail later, at generate):
Unknown options names, options without a value, and options set while config.messageOptions is missing or empty also throw at load.

Prompt message roles are narrowed

PromptMessage.role is now typed as 'system' | 'user' | 'assistant' instead of string, matching the authored role blocks accepted by loadPrompt(). Code that constructs a PromptMessage from a dynamic string must validate or narrow the value before assigning it.

Prompt bodies use explicit parsing modes

v0.11 searched the rendered body for supported role blocks wherever they appeared. This could silently ignore text outside those blocks, unknown top-level tags, and malformed attributes. v0.12 selects one mode from the first meaningful token after leading whitespace and HTML comments:
  • Plain text selects instruction mode. The whole trimmed body becomes prompt.instructions, including any tags that appear later.
  • A tag selects message mode. The complete body is validated as top-level role blocks and prompt.instructions is null.
These modes describe loadPrompt() output; API requirements are unchanged. generateText, generateTextWithStreaming, streamText, and Agent require message mode, while generateImage requires instruction mode. For example, v0.11 extracted the <user> block here and discarded the surrounding text:
In v0.12 this is one instruction string because it starts with text. To keep message mode, move the text inside the role block:
Message mode now enforces these rules:
  • Top-level blocks must use system, user, or assistant.
  • Only whitespace and HTML comments may appear between blocks.
  • Root self-closing tags, unmatched closing tags, and unclosed blocks throw.
  • Different-name tags inside a message remain literal message content.
  • A nested non-self-closing tag with the same name as its message throws instead of prematurely closing the outer block. Escape literal examples, such as &lt;user&gt;example&lt;/user&gt;.
  • Attribute names, separators, and quotes are validated. Spaces around = and > inside quoted values are supported; malformed fragments no longer pass silently.
Prompt files no longer accept authored <tool> blocks. Their string content never matched AI SDK’s structured tool-result message contract, so text generation rejected them. AI SDK continues to create tool messages during execution, and Agent callers may supply structured tool messages through messages or messageStore.

LLM trace details

Start-trace input on generateText, streamText, generateTextWithStreaming, generateImage, and Agent (generate, generateWithStreaming, stream) is the loaded prompt object only. Filename, interpolation values, and rendered config live on that object. Agent traces use the same shape as the text APIs (v0.11 recorded only the filename as prompt). End-trace output keeps the raw AI SDK usage and replaces tool-only sourcesFromTools with merged sources (tool results plus native provider sources; always an array). Normalized usage and cost are stored as trace attributes.

Start (input)

Before

After

If you read input.prompt as a filename, use input.prompt.name. If you read input.loadedPrompt, switch to input.prompt. If you read sibling input.variables, use input.prompt.variables.

End (output)

Before

After

If you read output.sourcesFromTools, switch to output.sources.

Attributes

The raw AI SDK output.usage shape remains on the trace output. Normalized usage and cost live under the LLM node’s attributes object:
The legacy attributes["llm:usage"] payload remains available for compatibility, so existing trace readers do not need an immediate migration. New integrations should use attributes["llm:generation:usage"] and attributes["llm:generation:cost"]; these preserve normalized usage independently from pricing and represent detailed dimensions explicitly. The CLI accepts all three keys and prefers the normalized attributes.

Response source, cost, and metering types

ExtractedSource now matches the AI SDK source union. A source can be a URL or a document, so narrow on sourceType before reading url:
response.cost and direct-stream onEnd cost now use LLMGenerationCost | null. The old LLMCallCost type is removed. @outputai/core no longer exports the LLM-specific Attribute.LLMUsage class or Attribute.Usage interface. Core now exposes only the generic Attribute.BaseAttribute for package-owned trace attributes. Import LLMGenerationUsage, LLMGenerationCost, and the deprecated legacy LLMUsageEvent type from @outputai/llm. In v0.11, response.cost used the legacy llm:usage shape with priced lines under usage. In v0.12, read aggregate costs directly and detailed dimensions from items:
The new object has type: "llm:generation:cost", providerId, modelId, nullable input, output, and total, a calculation status, and one cost item for each normalized usage item. Do not use only response.cost === null to detect unavailable totals. Missing model or dimension pricing returns an incomplete LLMGenerationCost; check response.cost?.total == null or inspect its status and item statuses. response.cost itself is null when the pricing catalog could not be loaded. LLMUsageEvent remains exported from @outputai/llm for cost:llm:request. Its legacy payload shape remains compatible, but the type is deprecated to guide new integrations toward the more complete metering event.
Use llm:generation:metering for new integrations when you need normalized usage and cost data. It fires when usage can be normalized; event.payload.cost may be null or incomplete. Existing cost:llm:request handlers can remain until you choose to adopt the more detailed format.

AI SDK helpers come from aiSdk

@outputai/llm no longer re-exports tool, Output, smoothStream, stepCountIs, hasToolCall, or jsonSchema as named exports. The namespace re-export ai is renamed to aiSdk.

Before

After

import { ai } from '@outputai/llm' becomes import { aiSdk } from '@outputai/llm'. Output APIs (generateText, Agent, loadPrompt, …) stay named exports. Cherry-picked AI SDK type re-exports (ToolSet, FinishReason, ModelMessage, StreamTextOnChunkCallback, …) are also gone. Import those from ai, or as aiSdk.ToolSet.

Replace removed Output option types

The Output-owned AI SDK option aliases were removed with the unrestricted native arguments. Use the corresponding public parameter type:
OutputAgentGenerateWithStreamingParameters no longer accepts an output type argument. Remove the generic:

Call arguments are a fixed list

Dropped native AI SDK call arguments from generateText(), generateTextWithStreaming(), streamText(), generateImage(), and Agent. Calls no longer accept temperature, maxOutputTokens, maxTokens, maxSteps, providerOptions, image n/size/seed, experimental_transform, onStepFinish, and similar. Unknown keys throw. Set model and generation or image config (including maxSteps, default 10) on the prompt file; call-argument stopWhen still overrides it. generateImage mask still requires images. Agent.generate(), Agent.generateWithStreaming(), and Agent.stream() append to messageStore only when finishReason is not 'error'. Errored turns are not persisted. streamText() and Agent.stream() now treat onError as a fire-and-forget observer. Output maps and forwards the provider error, but exceptions and rejected promises from the callback are ignored. To fail a workflow step with the original error, capture it in onError and throw it after consuming the stream.

Agent message store

conversationStore is renamed to messageStore. The type is MessageStore. createMemoryConversationStore() is removed; implement the store yourself.

Before

After

Move model config onto the prompt

Before

After

prompts/writer@v1.prompt
Any merged tools, including prompt-only Vertex googleSearch / urlContext, now get stopWhen: isStepCount(maxSteps) from that prompt value. Previously the ceiling applied only when call-argument tools or load_skill were present; YAML-only grounding stayed at the AI SDK default of one step. If you need the old one-step grounding behavior, set maxSteps: 1 on the prompt, or pass stopWhen on the call:

Agent constructor validation

new Agent( {} ) no longer throws Agent requires a prompt. Invalid constructor args use the same schema as generateText and throw Invalid Agent() arguments. That includes a missing/empty prompt, an empty promptDir, and call-argument skills or maxSteps fields.

Prompt config is a strict key list

Unknown top-level keys on a .prompt file now throw Invalid prompt file. Previously they were kept on config and ignored. provider and model must be non-empty strings. When provided, maxOutputTokens and deprecated maxTokens must each be a positive integer. Nested providerOptions (including thinking) stays open. Allowed top-level keys: provider, model, temperature, maxOutputTokens, deprecated maxTokens, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, maxSteps, skills, tools, providerOptions, messageOptions, n, maxImagesPerCall, size, aspectRatio. maxTokens remains available for backward compatibility and is retained on the loaded prompt config. When maxOutputTokens is absent, loadPrompt also copies the maxTokens value to maxOutputTokens. When both keys are set, maxOutputTokens takes precedence. Text generation APIs read the normalized maxOutputTokens value. Snake_case aliases of those keys fail with a suggestion:
Move provider-specific fields under providerOptions. effort and reasoningEffort at the top level are unknown keys; they belong under providerOptions.anthropic and providerOptions.openai.

Before

After

Checklist

  • Delete skills from generateText / streamText / generateTextWithStreaming / Agent calls.
  • Remove skill(), Skill, and SkillsArg imports; move inline skills into files listed under prompt skills:.
  • Add skills: ./skills (or explicit file paths) to prompts that relied on colocated auto-discovery.
  • Expect YAML tools and call-argument tools to merge; remove YAML tools if you previously relied on replacement.
  • Strip dropped call arguments from generateText / generateTextWithStreaming / streamText / generateImage / Agent (temperature, maxOutputTokens, maxTokens, maxSteps, providerOptions, maxRetries, experimental_transform, onStepFinish, image n / size / seed, and any other AI SDK-only keys). Unknown keys throw. Put model and generation or image config on the prompt file; call-argument stopWhen still overrides maxSteps.
  • Update installed AI SDK packages to ai 7.x, OpenAI / Anthropic / Azure / Perplexity 4.x, and Amazon Bedrock / Google Vertex 5.x.
  • Rename onFinish to onEnd in streamText() and Agent.stream(). Rename the corresponding wrapped callback and event types.
  • Replace fullStream with stream, and update exhaustive onChunk handlers for the expanded AI SDK 7 part union.
  • Replace stepCountIs() with isStepCount().
  • Review direct AI SDK result-field access against the official AI SDK 7 migration guide.
  • Use group and label when reading normalized LLMGenerationUsage and LLMGenerationCost items. Legacy event lines retain their existing type names.
  • Remove assumptions that Output’s Agent inherits from AI SDK’s ToolLoopAgent.
  • Preserve binary file and reasoning-file content parts in custom MessageStore serializers.
  • Set providerOptions.openai.reasoningSummary: null on reasoning prompts that should not return summaries.
  • Set maxSteps: 1 on YAML-only grounding prompts that must stay one-shot, or pass stopWhen: aiSdk.isStepCount(1) on the call.
  • Rename promptFileDir to fileDir. Read interpolation values from prompt.variables.
  • Treat config.skills as always string[] after loadPrompt.
  • Treat config.maxSteps as always a positive integer after loadPrompt (default 10).
  • Treat prompt.instructions as always string | null after loadPrompt (chat prompts are null).
  • Read message.providerOptions instead of message.attributes on loadPrompt results and LLM trace input.prompt.messages.
  • Narrow dynamic role strings before assigning them to PromptMessage.role; the type now accepts only 'system', 'user', and 'assistant'.
  • Remove unknown attributes from role tags (name, id, pinned, …). Only options is allowed; extras throw at loadPrompt.
  • Remove authored <tool> blocks from prompt files; pass structured tool history through Agent messages or messageStore.
  • Audit prompt bodies that put prose before the first role tag. They now load as instructions; move the prose inside a role block to keep message mode.
  • Remove text between or after top-level role blocks. Only whitespace and HTML comments are allowed there.
  • Escape literal same-name role tags inside messages (&lt;user&gt;...&lt;/user&gt;). Different-name semantic tags remain valid content.
  • Give every options attribute a value and fix malformed attribute names or quotes; prompt markup now fails explicitly at load.
  • Read LLM trace input.prompt as the loaded prompt object (input.prompt.name, input.prompt.variables). Do not treat input.prompt as a filename or read input.loadedPrompt.
  • Read LLM trace output.sources instead of output.sourcesFromTools.
  • Keep existing llm:usage trace readers for compatibility, but prefer llm:generation:usage and llm:generation:cost for normalized trace metadata.
  • Narrow ExtractedSource on sourceType before reading url. Replace LLMCallCost with LLMGenerationCost, replace response.cost.usage reads with response.cost.items, and handle an incomplete cost whose total is null. Import LLM generation usage and cost types from @outputai/llm, not @outputai/core.
  • Keep existing cost:llm:request handlers for structural compatibility, while allowing corrected reasoning values. Prefer llm:generation:metering with LLMGenerationMeteringEvent for new normalized usage/cost integrations.
  • Import AI SDK helpers and types from aiSdk (aiSdk.Output, aiSdk.tool, aiSdk.isStepCount, aiSdk.ToolSet, …). Replace import { ai } with import { aiSdk }. Do not import cherry-picked AI SDK types from @outputai/llm.
  • Replace GenerateTextAiSdkOptions, StreamTextAiSdkOptions, and GenerateImageAiSdkOptions with their *Parameters equivalents. Remove the generic from OutputAgentGenerateWithStreamingParameters.
  • Expect Agent.generate(), Agent.generateWithStreaming(), and Agent.stream() to persist message-store history only when finishReason is not 'error'.
  • Replace conversationStore with messageStore. Replace ConversationStore with MessageStore. Remove createMemoryConversationStore() and pass your own store.
  • Update Agent tests and error matchers that expected Agent requires a prompt.
  • Ensure prompt provider and model values are non-empty. If set, maxOutputTokens and deprecated maxTokens must each be positive integers.
  • Review prompt frontmatter against the supported configuration and remove unsupported fields. Any remaining fields cause an Invalid prompt file error.