AI SDK 4.x → 5.0 마이그레이션

AI SDK 4.x → 5.0 마이그레이션 (Migrate AI SDK 4.x to 5.0)

AI SDK 4.x에서 5.0으로 업그레이드하는 방법을 정리한 가이드예요. 5.0은 메시지/타입 시스템(CoreMessage → ModelMessage, Message → UIMessage), 스트리밍 아키텍처 전면 재설계, UI 훅 구조 변경(@ai-sdk/react, @ai-sdk/rsc 분리), stopWhen 기반 멀티스텝 제어, OpenAI Responses API 기본 적용 등 방대한 변경을 포함합니다. 코드마이그레이션은 codemod나 MCP 서버로 자동화할 수 있어요.

출처: 문서

본문

  1. 프로젝트를 백업하세요. 버전 관리 시스템을 쓴다면 이전 버전이 모두 커밋되어 있는지 확인하세요.
  2. AI SDK 5.0으로 업그레이드하세요.
  3. 다음 방식 중 하나로 코드를 자동 마이그레이션하세요:
  4. 아래의 breaking changes 가이드를 따르세요.
  5. 프로젝트가 예상대로 동작하는지 확인하세요.
  6. 변경 사항을 커밋하세요.

AI SDK 5 Migration MCP Server

AI SDK 5 Migration Model Context Protocol (MCP) Server는 코딩 에이전트를 사용해 프로젝트를 자동 마이그레이션하는 방법을 제공해요. 이 서버는 Cursor용으로 설계되었지만 MCP를 지원하는 어떤 코딩 에이전트에서도 동작합니다.

시작하려면 프로젝트에 .cursor/mcp.json을 만들거나 편집하세요:

{
  "mcpServers": {
    "ai-sdk-5-migration": {
      "url": "https://ai-sdk-5-migration-mcp-server.vercel.app/api/mcp"
    }
  }
}

저장한 뒤 명령 팔레트(macOS는 Cmd+Shift+P, Windows/Linux는 Ctrl+Shift+P)를 열고 "View: Open MCP Settings"를 검색하세요. 새 서버가 나타나고 켜져 있는지 확인하세요.

그런 다음 이 프롬프트를 사용하세요:

Please migrate this project to AI SDK 5 using the ai-sdk-5-migration mcp server. Start by creating a checklist.

자세한 내용은 AI SDK 5 Migration MCP Server 저장소를 참고하세요.

AI SDK 5.0 패키지 버전 (AI SDK 5.0 Package Versions)

package.json 파일에서 다음 패키지들을 해당 버전으로 업데이트해야 해요:

  • ai 패키지: 5.0.0
  • @ai-sdk/provider 패키지: 2.0.0
  • @ai-sdk/provider-utils 패키지: 3.0.0
  • @ai-sdk/* 패키지: 2.0.0 (기타 @ai-sdk 패키지)

또한 다음 peer dependency도 업데이트해야 합니다:

  • zod 패키지: 4.1.8 이상 (TypeScript 성능 문제를 피하려면 권장)

업그레이드 명령 예시:

npm install ai @ai-sdk/react @ai-sdk/openai zod@^4.1.8
업그레이드 후 TypeScript 성능 문제가 발생하면 Zod 4.1.8 이상을 사용하고 있는지 확인하세요. 문제가 지속되면 `tsconfig.json`의 `moduleResolution: "nodenext"` 설정을 사용하세요. 자세한 내용은 [TypeScript performance troubleshooting guide](/docs/troubleshooting/typescript-performance-zod)를 참고하세요.

Codemods

AI SDK는 기능이 deprecated 되거나, 제거되거나, 변경될 때 코드베이스 업그레이드를 돕기 위한 Codemod 변환을 제공해요.

Codemod는 코드베이스에서 자동으로 실행되는 변환이에요. 모든 파일을 수동으로 훑지 않고도 많은 변경을 쉽게 적용할 수 있게 해줍니다.

Codemod는 업그레이드 과정을 돕기 위한 도구예요. 필요한 모든 변경을 다루지 못할 수도 있으며, 추가 변경을 수동으로 해야 할 수도 있어요.

프로젝트 루트에서 다음 명령을 실행하면 5.0 업그레이드 과정의 일부로 제공되는 모든 codemod를 실행할 수 있어요:

npx @ai-sdk/codemod upgrade

v5 codemod만 실행하려면 (v4 → v5 마이그레이션):

npx @ai-sdk/codemod v5

개별 codemod는 이름을 지정해 실행할 수 있어요:

npx @ai-sdk/codemod <codemod-name> <path>

예를 들어 특정 v5 codemod를 실행하려면:

npx @ai-sdk/codemod v5/rename-format-stream-part src/

codemod 표도 참고하세요. 최신 codemod 세트는 @ai-sdk/codemod 저장소에서 확인할 수 있습니다.

AI SDK Core 변경 사항 (AI SDK Core Changes)

generateText와 streamText 변경 사항

최대 출력 토큰 (Maximum Output Tokens)

maxTokens 파라미터가 명확성을 위해 maxOutputTokens로 변경됐어요.

const result = await generateText({
  model: __MODEL__,
  maxTokens: 1024,
  prompt: 'Hello, world!',
});
const result = await generateText({
  model: __MODEL__,
  maxOutputTokens: 1024,
  prompt: 'Hello, world!',
});

메시지 및 타입 시스템 변경 사항 (Message and Type System Changes)

Core 타입 이름 변경 (Core Type Renames)
CoreMessage → ModelMessage
import { CoreMessage } from 'ai';
import { ModelMessage } from 'ai';
Message → UIMessage
import { Message, CreateMessage } from 'ai';
import { UIMessage, CreateUIMessage } from 'ai';
convertToCoreMessages → convertToModelMessages
import { convertToCoreMessages, streamText } from 'ai';

const result = await streamText({
  model: __MODEL__,
  messages: convertToCoreMessages(messages),
});
import { convertToModelMessages, streamText } from 'ai';

const result = await streamText({
  model: __MODEL__,
  messages: convertToModelMessages(messages),
});
모델 메시지에 대한 자세한 내용은 [Model Message reference](/docs/reference/ai-sdk-core/model-message)를 참고하세요.

UIMessage 변경 사항 (UIMessage Changes)

content → parts 배열 (Content → Parts Array)

UIMessage(이전 Message)의 경우 .content 속성이 parts 배열 구조로 대체됐어요.

import { type Message } from 'ai'; // v4 Message type

// Messages (useChat) - had content property
const message: Message = {
  id: '1',
  role: 'user',
  content: 'Bonjour!',
};
import { type UIMessage, type ModelMessage } from 'ai';

// UIMessages (useChat) - now use parts array
const uiMessage: UIMessage = {
  id: '1',
  role: 'user',
  parts: [{ type: 'text', text: 'Bonjour!' }],
};

data role 제거 (Data Role Removed)

UI 메시지에서 data role이 제거됐어요.

const message = {
  role: 'data',
  content: 'Some content',
  data: { customField: 'value' },
};
// V5: Use UI message streams with custom data parts
const stream = createUIMessageStream({
  execute({ writer }) {
    // Write custom data instead of message annotations
    writer.write({
      type: 'data-custom',
      id: 'custom-1',
      data: { customField: 'value' },
    });
  },
});

UIMessage reasoning 구조 (UIMessage Reasoning Structure)

UI 메시지의 reasoning 속성이 parts로 이동했어요.

const message: Message = {
  role: 'assistant',
  content: 'Hello',
  reasoning: 'I will greet the user',
};
const message: UIMessage = {
  role: 'assistant',
  parts: [
    {
      type: 'reasoning',
      text: 'I will greet the user',
    },
    {
      type: 'text',
      text: 'Hello',
    },
  ],
};

reasoning part 속성 이름 변경 (Reasoning Part Property Rename)

reasoning UI part의 reasoning 속성이 text로 변경됐어요.

{
  message.parts.map((part, index) => {
    if (part.type === 'reasoning') {
      return (
        <div key={index} className="reasoning-display">
          {part.reasoning}
        </div>
      );
    }
  });
}
{
  message.parts.map((part, index) => {
    if (part.type === 'reasoning') {
      return (
        <div key={index} className="reasoning-display">
          {part.text}
        </div>
      );
    }
  });
}

File part 변경 사항 (File Part Changes)

File part는 이제 .data와 .mimeType 대신 .url을 사용해요.

{
  messages.map(message => (
    <div key={message.id}>
      {message.parts.map((part, index) => {
        if (part.type === 'text') {
          return <div key={index}>{part.text}</div>;
        } else if (part.type === 'file' && part.mimeType.startsWith('image/')) {
          return (
            <img
              key={index}
              src={`data:${part.mimeType};base64,${part.data}`}
            />
          );
        }
      })}
    </div>
  ));
}
{
  messages.map(message => (
    <div key={message.id}>
      {message.parts.map((part, index) => {
        if (part.type === 'text') {
          return <div key={index}>{part.text}</div>;
        } else if (
          part.type === 'file' &&
          part.mediaType.startsWith('image/')
        ) {
          return <img key={index} src={part.url} />;
        }
      })}
    </div>
  ));
}

Stream Data 제거 (Stream Data Removal)

StreamData 클래스가 완전히 제거되고, 커스텀 데이터는 UI 메시지 스트림으로 대체됐어요.

import { StreamData } from 'ai';

const streamData = new StreamData();
streamData.append('custom-data');
streamData.close();
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai';

const stream = createUIMessageStream({
  execute({ writer }) {
    // Write custom data parts
    writer.write({
      type: 'data-custom',
      id: 'custom-1',
      data: 'custom-data',
    });

    // Can merge with LLM streams
    const result = streamText({
      model: __MODEL__,
      messages,
    });

    writer.merge(result.toUIMessageStream());
  },
});

return createUIMessageStreamResponse({ stream });

커스텀 데이터 스트리밍: writeMessageAnnotation/writeData 제거

DataStreamWriter의 writeMessageAnnotation과 writeData 메서드가 제거됐어요. 대신 새 UIMessage 스트림 아키텍처의 커스텀 데이터 part를 사용하세요.

import { createDataStreamResponse, streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  return createDataStreamResponse({
    execute: dataStream => {
      // Write general data
      dataStream.writeData('call started');

      const result = streamText({
        model: __MODEL__,
        messages,
        onChunk() {
          // Write message annotations
          dataStream.writeMessageAnnotation({
            status: 'streaming',
            timestamp: Date.now(),
          });
        },
        onFinish() {
          // Write final annotations
          dataStream.writeMessageAnnotation({
            id: generateId(),
            completed: true,
          });

          dataStream.writeData('call completed');
        },
      });

      result.mergeIntoDataStream(dataStream);
    },
  });
}
import {
  createUIMessageStream,
  createUIMessageStreamResponse,
  streamText,
  generateId,
} from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const stream = createUIMessageStream({
    execute: ({ writer }) => {
      const statusId = generateId();

      // Write general data (transient - not added to message history)
      writer.write({
        type: 'data-status',
        id: statusId,
        data: { status: 'call started' },
      });

      const result = streamText({
        model: __MODEL__,
        messages,
        onChunk() {
          // Write data parts that update during streaming
          writer.write({
            type: 'data-status',
            id: statusId,
            data: {
              status: 'streaming',
              timestamp: Date.now(),
            },
          });
        },
        onFinish() {
          // Write final data parts
          writer.write({
            type: 'data-status',
            id: statusId,
            data: {
              status: 'completed',
            },
          });
        },
      });

      writer.merge(result.toUIMessageStream());
    },
  });

  return createUIMessageStreamResponse({ stream });
}
v5에서 커스텀 데이터 스트리밍에 대한 더 자세한 내용은 [Streaming Data guide](/docs/ai-sdk-ui/streaming-data)를 참고하세요.
Provider Metadata → Provider Options

providerMetadata 입력 파라미터가 providerOptions로 이름이 변경됐어요. 결과에서 반환되는 메타데이터는 여전히 providerMetadata라고 불린다는 점을 기억하세요.

const result = await generateText({
  model: 'openai/gpt-5',
  prompt: 'Hello',
  providerMetadata: {
    openai: { store: false },
  },
});
const result = await generateText({
  model: 'openai/gpt-5',
  prompt: 'Hello',
  providerOptions: {
    // Input parameter renamed
    openai: { store: false },
  },
});

// Returned metadata still uses providerMetadata:
console.log(result.providerMetadata?.openai);

Tool 정의 변경 사항 (parameters → inputSchema)

Tool 정의가 parameters 대신 inputSchema를 사용하도록 업데이트되었고, 오류 클래스 이름도 변경됐어요.

import { tool } from 'ai';

const weatherTool = tool({
  description: 'Get the weather for a city',
  parameters: z.object({
    city: z.string(),
  }),
  execute: async ({ city }) => {
    return `Weather in ${city}`;
  },
});
import { tool } from 'ai';

const weatherTool = tool({
  description: 'Get the weather for a city',
  inputSchema: z.object({
    city: z.string(),
  }),
  execute: async ({ city }) => {
    return `Weather in ${city}`;
  },
});

Tool 결과 콘텐츠: experimental_toToolResultContent → toModelOutput

experimental_toToolResultContent 옵션이 toModelOutput로 이름이 바뀌고 더 이상 experimental이 아니에요.

const screenshotTool = tool({
  description: 'Take a screenshot',
  parameters: z.object({}),
  execute: async () => {
    const imageData = await takeScreenshot();
    return imageData; // base64 string
  },
  experimental_toToolResultContent: result => [{ type: 'image', data: result }],
});
const screenshotTool = tool({
  description: 'Take a screenshot',
  inputSchema: z.object({}),
  execute: async () => {
    const imageData = await takeScreenshot();
    return imageData;
  },
  toModelOutput: result => ({
    type: 'content',
    value: [{ type: 'media', mediaType: 'image/png', data: result }],
  }),
});

Tool 속성 변경 사항 (args/result → input/output)

Tool 호출과 결과 속성이 스키마와의 일관성을 위해 이름이 변경됐어요.

// Tool calls used "args" and "result"
for await (const part of result.fullStream) {
  switch (part.type) {
    case 'tool-call':
      console.log('Tool args:', part.args);
      break;
    case 'tool-result':
      console.log('Tool result:', part.result);
      break;
  }
}
// Tool calls now use "input" and "output"
for await (const part of result.fullStream) {
  switch (part.type) {
    case 'tool-call':
      console.log('Tool input:', part.input);
      break;
    case 'tool-result':
      console.log('Tool output:', part.output);
      break;
  }
}

Tool 실행 오류 처리 (Tool Execution Error Handling)

ToolExecutionError 클래스가 제거됐어요. Tool 실행 오류는 이제 결과 steps의 tool-error 콘텐츠 part로 나타나며, 멀티스텝 시나리오에서 자동 LLM 왕복(roundtrip)을 가능하게 합니다.

import { ToolExecutionError } from 'ai';

try {
  const result = await generateText({
    // ...
  });
} catch (error) {
  if (error instanceof ToolExecutionError) {
    console.log('Tool execution failed:', error.message);
    console.log('Tool name:', error.toolName);
    console.log('Tool input:', error.toolInput);
  }
}
// Tool execution errors now appear in result steps
const { steps } = await generateText({
  // ...
});

// check for tool errors in the steps
const toolErrors = steps.flatMap(step =>
  step.content.filter(part => part.type === 'tool-error'),
);

toolErrors.forEach(toolError => {
  console.log('Tool error:', toolError.error);
  console.log('Tool name:', toolError.toolName);
  console.log('Tool input:', toolError.input);
});

스트리밍 시나리오에서는 tool 실행 오류가 스트림의 tool-error part로 나타나고, 다른 오류는 error part로 나타납니다.

Tool 호출 스트리밍 기본 활성화 (toolCallStreaming 제거)

AI SDK 5.0에서 toolCallStreaming 옵션이 제거됐어요. Tool 호출 스트리밍은 이제 기본적으로 항상 활성화됩니다.

const result = streamText({
  model: __MODEL__,
  messages,
  toolCallStreaming: true, // Optional parameter to enable streaming
  tools: {
    weatherTool,
    searchTool,
  },
});
const result = streamText({
  model: __MODEL__,
  messages: convertToModelMessages(messages),
  // toolCallStreaming removed - streaming is always enabled
  tools: {
    weatherTool,
    searchTool,
  },
});

Tool part 타입 변경 사항 (UIMessage)

v5에서 UI tool part는 일반 타입 대신 tool-${toolName} 형식의 타입 지정 이름을 사용해요.

// Generic tool-invocation type
{
  message.parts.map(part => {
    if (part.type === 'tool-invocation') {
      return <div>{part.toolInvocation.toolName}</div>;
    }
  });
}
// Type-safe tool parts with specific names
{
  message.parts.map(part => {
    switch (part.type) {
      case 'tool-getWeatherInformation':
        return <div>Getting weather...</div>;
      case 'tool-askForConfirmation':
        return <div>Asking for confirmation...</div>;
    }
  });
}

동적 tool 지원 (Dynamic Tools Support)

AI SDK 5.0은 개발 시점에 타입을 알 수 없는 tool(예: 스키마가 없는 MCP tool, 런타임에 정의되는 사용자 함수)을 처리하기 위한 동적 tool을 도입했어요.

새 dynamicTool 헬퍼 (New dynamicTool Helper)

새 dynamicTool 헬퍼 함수는 입력/출력 타입이 컴파일 시점에 알려지지 않은 tool을 정의할 수 있게 해줍니다.

import { dynamicTool } from 'ai';
import { z } from 'zod';

// Define a dynamic tool
const runtimeTool = dynamicTool({
  description: 'A tool defined at runtime',
  inputSchema: z.object({}),
  execute: async input => {
    // Input and output are typed as 'unknown'
    return { result: `Processed: ${input.query}` };
  },
});

스키마가 없는 MCP tool (MCP Tools Without Schemas)

스키마를 제공하지 않는 MCP tool은 이제 자동으로 동적 tool로 처리됩니다:

import { MCPClient } from 'ai';

const client = new MCPClient({
  /* ... */
});
const tools = await client.getTools();

// Tools without schemas are now 'dynamic' type
// and won't break type inference when mixed with static tools

혼합 tool과 타입 안전 처리 (Type-Safe Handling with Mixed Tools)

정적 tool과 동적 tool을 함께 사용할 때는 타입 좁히기를 위해 dynamic 플래그를 사용하세요:

const result = await generateText({
  model: __MODEL__,
  tools: {
    // Static tool with known types
    weather: weatherTool,
    // Dynamic tool with unknown types
    customDynamicTool: dynamicTool({
      /* ... */
    }),
  },
  onStepFinish: step => {
    // Handle tool calls with type safety
    for (const toolCall of step.toolCalls) {
      if (toolCall.dynamic) {
        // Dynamic tool: input/output are 'unknown'
        console.log('Dynamic tool called:', toolCall.toolName);
        continue;
      }

      // Static tools have full type inference
      switch (toolCall.toolName) {
        case 'weather':
          // TypeScript knows the exact types
          console.log(toolCall.input.location); // string
          break;
      }
    }
  },
});

새 dynamic-tool UI part (New dynamic-tool UI Part)

UI 메시지에는 이제 동적 tool 호출을 렌더링하기 위한 dynamic-tool part 타입이 포함됩니다:

{
  message.parts.map((part, index) => {
    switch (part.type) {
      // Static tools use specific types
      case 'tool-weather':
        return <div>Weather: {part.input.city}</div>;

      // Dynamic tools use the generic dynamic-tool type
      case 'dynamic-tool':
        return (
          <div>
            Dynamic tool: {part.toolName}
            <pre>{JSON.stringify(part.input, null, 2)}</pre>
          </div>
        );
    }
  });
}

Breaking Change: tool 호출/결과의 타입 좁히기 필수

toolCalls와 toolResults를 순회할 때 이제 올바른 타입 좁히기를 위해 먼저 dynamic 플래그를 확인해야 해요:

// Direct type checking worked without dynamic flag
onStepFinish: step => {
  for (const toolCall of step.toolCalls) {
    switch (toolCall.toolName) {
      case 'weather':
        console.log(toolCall.input.location); // typed as string
        break;
      case 'search':
        console.log(toolCall.input.query); // typed as string
        break;
    }
  }
};
// Must check dynamic flag first for type narrowing
onStepFinish: step => {
  for (const toolCall of step.toolCalls) {
    // Check if it's a dynamic tool first
    if (toolCall.dynamic) {
      console.log('Dynamic tool:', toolCall.toolName);
      console.log('Input:', toolCall.input); // typed as unknown
      continue;
    }

    // Now TypeScript knows it's a static tool
    switch (toolCall.toolName) {
      case 'weather':
        console.log(toolCall.input.location); // typed as string
        break;
      case 'search':
        console.log(toolCall.input.query); // typed as string
        break;
    }
  }
};

Tool UI part 상태 변경 사항 (Tool UI Part State Changes)

Tool UI part는 이제 스트리밍 수명주기와 오류 처리를 더 잘 표현하는 세분화된 상태를 사용해요.

// Old states
{
  message.parts.map(part => {
    if (part.type === 'tool-invocation') {
      switch (part.toolInvocation.state) {
        case 'partial-call':
          return <div>Loading...</div>;
        case 'call':
          return (
            <div>
              Tool called with {JSON.stringify(part.toolInvocation.args)}
            </div>
          );
        case 'result':
          return <div>Result: {part.toolInvocation.result}</div>;
      }
    }
  });
}
// New granular states
{
  message.parts.map(part => {
    switch (part.type) {
      case 'tool-getWeatherInformation':
        switch (part.state) {
          case 'input-streaming':
            return <pre>{JSON.stringify(part.input, null, 2)}</pre>;
          case 'input-available':
            return <div>Getting weather for {part.input.city}...</div>;
          case 'output-available':
            return <div>Weather: {part.output}</div>;
          case 'output-error':
            return <div>Error: {part.errorText}</div>;
        }
    }
  });
}

상태 변경 사항 (State Changes):

  • partial-call → input-streaming (tool 입력 스트리밍 중)
  • call → input-available (tool 입력 완료, 실행 준비됨)
  • result → output-available (tool 실행 성공)
  • 새로 추가: output-error (tool 실행 실패)

Tool 호출 렌더링 (Catch-All 패턴)

v4에서는 일반적으로 catch-all tool-invocation 타입으로 tool 호출을 렌더링했어요. v5에서는 각 tool을 타입 지정 part 이름(예: tool-getWeather)으로 개별 처리하는 것이 권장됩니다. 하지만 모든 tool 호출을 같은 방식으로 렌더링하는 catch-all 패턴이 필요하다면 isToolUIPart와 getToolName 헬퍼 함수를 폴백으로 사용할 수 있어요.

{
  message.parts.map((part, index) => {
    switch (part.type) {
      case 'text':
        return <div key={index}>{part.text}</div>;
      case 'tool-invocation':
        const { toolInvocation } = part;
        return (
          <details key={`tool-${toolInvocation.toolCallId}`}>
            <summary>
              <span>{toolInvocation.toolName}</span>
              {toolInvocation.state === 'result' ? (
                <span>Click to expand</span>
              ) : (
                <span>calling...</span>
              )}
            </summary>
            {toolInvocation.state === 'result' ? (
              <div>
                <pre>{JSON.stringify(toolInvocation.result, null, 2)}</pre>
              </div>
            ) : null}
          </details>
        );
    }
  });
}
import { isToolUIPart, getToolName } from 'ai';

{
  message.parts.map((part, index) => {
    switch (part.type) {
      case 'text':
        return <div key={index}>{part.text}</div>;
      default:
        if (isToolUIPart(part)) {
          const toolInvocation = part;
          return (
            <details key={`tool-${toolInvocation.toolCallId}`}>
              <summary>
                <span>{getToolName(toolInvocation)}</span>
                {toolInvocation.state === 'output-available' ? (
                  <span>Click to expand</span>
                ) : (
                  <span>calling...</span>
                )}
              </summary>
              {toolInvocation.state === 'output-available' ? (
                <div>
                  <pre>{JSON.stringify(toolInvocation.output, null, 2)}</pre>
                </div>
              ) : null}
            </details>
          );
        }
    }
  });
}

미디어 타입 표준화 (Media Type Standardization)

일관성을 위해 mimeType이 mediaType으로 이름이 변경됐어요. 이미지와 파일 타입 모두 모델 메시지에서 지원됩니다.

const result = await generateText({
  model: someModel,
  messages: [
    {
      role: 'user',
      content: [
        { type: 'text', text: 'What do you see?' },
        {
          type: 'image',
          image: new Uint8Array([0, 1, 2, 3]),
          mimeType: 'image/png',
        },
        {
          type: 'file',
          data: contents,
          mimeType: 'application/pdf',
        },
      ],
    },
  ],
});
const result = await generateText({
  model: someModel,
  messages: [
    {
      role: 'user',
      content: [
        { type: 'text', text: 'What do you see?' },
        {
          type: 'image',
          image: new Uint8Array([0, 1, 2, 3]),
          mediaType: 'image/png',
        },
        {
          type: 'file',
          data: contents,
          mediaType: 'application/pdf',
        },
      ],
    },
  ],
});

Reasoning 지원 (Reasoning Support)

Reasoning 텍스트 속성 이름 변경 (Reasoning Text Property Rename)

멀티스텝 생성에서 .reasoning 속성이 .reasoningText로 변경됐어요.

for (const step of steps) {
  console.log(step.reasoning);
}
for (const step of steps) {
  console.log(step.reasoningText);
}

generateText reasoning 속성 변경 사항 (Generate Text Reasoning Property Changes)

generateText()와 streamText() 결과에서 reasoning 속성이 이름이 변경됐어요.

const result = await generateText({
  model: anthropic('claude-sonnet-4-20250514'),
  prompt: 'Explain your reasoning',
});

console.log(result.reasoning); // String reasoning text
console.log(result.reasoningDetails); // Array of reasoning details
const result = await generateText({
  model: anthropic('claude-sonnet-4-20250514'),
  prompt: 'Explain your reasoning',
});

console.log(result.reasoningText); // String reasoning text
console.log(result.reasoning); // Array of reasoning details

연속 스텝 제거 (Continuation Steps Removal)

experimental_continueSteps 옵션이 generateText()에서 제거됐어요.

const result = await generateText({
  experimental_continueSteps: true,
  // ...
});
const result = await generateText({
  // experimental_continueSteps has been removed
  // Use newer models with higher output token limits instead
  // ...
});

이미지 생성 변경 사항 (Image Generation Changes)

이미지 모델 설정이 providerOptions로 이동했어요.

await generateImage({
  model: luma.image('photon-flash-1', {
    maxImagesPerCall: 5,
    pollIntervalMillis: 500,
  }),
  prompt,
  n: 10,
});
await generateImage({
  model: luma.image('photon-flash-1'),
  prompt,
  n: 10,
  maxImagesPerCall: 5,
  providerOptions: {
    luma: { pollIntervalMillis: 500 },
  },
});

Step 결과 변경 사항 (Step Result Changes)

stepType 제거 (Step Type Removal)

stepType 속성이 step 결과에서 제거됐어요.

steps.forEach(step => {
  switch (step.stepType) {
    case 'initial':
      console.log('Initial step');
      break;
    case 'tool-result':
      console.log('Tool result step');
      break;
    case 'done':
      console.log('Final step');
      break;
  }
});
steps.forEach((step, index) => {
  if (index === 0) {
    console.log('Initial step');
  } else if (step.toolResults.length > 0) {
    console.log('Tool result step');
  } else {
    console.log('Final step');
  }
});

Step 제어: maxSteps → stopWhen

generateText와 streamText 같은 core 함수에서 maxSteps 파라미터가 stopWhen으로 대체되었어요. stopWhen은 멀티스텝 실행을 더 유연하게 제어합니다. stopWhen 파라미터는 마지막 step에 tool 결과가 있을 때 생성을 멈출 조건을 정의합니다. 여러 조건을 배열로 제공하면 그중 하나라도 충족되면 생성을 멈춥니다.

// V4: Simple numeric limit
const result = await generateText({
  model: __MODEL__,
  messages,
  maxSteps: 5, // Stop after a maximum of 5 steps
});

// useChat with maxSteps
const { messages } = useChat({
  maxSteps: 3, // Stop after a maximum of 3 steps
});
import { isStepCount, hasToolCall } from 'ai';

// V5: Server-side - flexible stopping conditions with stopWhen
const result = await generateText({
  model: __MODEL__,
  messages,
  // Only triggers when last step has tool results
  stopWhen: isStepCount(5), // Stop at step 5 if tools were called
});

// Server-side - stop when a specific tool is called
const result = await generateText({
  model: __MODEL__,
  messages,
  stopWhen: hasToolCall('finalizeTask'), // Stop when finalizeTask tool is called
});

일반적인 중지 패턴 (Common stopping patterns):

// Stop after N steps (equivalent to old maxSteps)
// Note: Only applies when the last step has tool results
stopWhen: isStepCount(5);

// Stop when a specific tool is called
stopWhen: hasToolCall('finalizeTask');

// Stop when either tool is called
stopWhen: hasToolCall('submitOrder', 'finalizeTask');

// Multiple conditions (stops if ANY condition is met)
stopWhen: [
  isStepCount(10), // Maximum 10 steps
  hasToolCall('submitOrder'), // Or when order is submitted
];

// Custom condition based on step content
stopWhen: ({ steps }) => {
  const lastStep = steps[steps.length - 1];
  // Custom logic - only triggers if last step has tool results
  return lastStep?.text?.includes('COMPLETE');
};

중요: stopWhen 조건은 마지막 step에 tool 결과가 있을 때만 평가됩니다.

usage vs totalUsage

Usage 속성이 이제 단일 step과 전체 사용량을 구분합니다.

// usage contained total token usage across all steps
console.log(result.usage);
// usage contains token usage from the final step only
console.log(result.usage);
// totalUsage contains total token usage across all steps
console.log(result.totalUsage);

AI SDK UI 변경 사항 (AI SDK UI Changes)

패키지 구조 변경 사항 (Package Structure Changes)

@ai-sdk/rsc 패키지 추출 (Package Extraction)

ai/rsc export가 @ai-sdk/rsc라는 별도 패키지로 분리됐어요.

import { createStreamableValue } from 'ai/rsc';
import { createStreamableValue } from '@ai-sdk/rsc';

새 패키지 설치를 잊지 마세요: npm install @ai-sdk/rsc

React UI 훅이 @ai-sdk/react로 이동 (React UI Hooks Moved to @ai-sdk/react)

deprecated 된 ai/react export가 @ai-sdk/react를 위해 제거됐어요.

import { useChat } from 'ai/react';
import { useChat } from '@ai-sdk/react';
새 패키지 설치를 잊지 마세요: `npm install @ai-sdk/react`

useChat 변경 사항 (useChat Changes)

useChat 훅은 v5에서 새 transport 아키텍처, 관리형 입력 상태 제거 등 큰 변화를 겪었어요.

maxSteps 제거 (maxSteps Removal)

maxSteps 파라미터가 useChat에서 제거됐어요. 이제 멀티스텝 tool 실행 제어는 서버 측 stopWhen 조건을 사용하고, 클라이언트 측 tool 호출은 tool 결과를 수동으로 제출하고 새 메시지를 트리거해야 합니다.

const { messages, sendMessage } = useChat({
  maxSteps: 5, // Automatic tool result submission
});
// Server-side: Use stopWhen for multi-step control
import { streamText, convertToModelMessages, isStepCount } from 'ai';
__PROVIDER_IMPORT__;

const result = await streamText({
  model: __MODEL__,
  messages: convertToModelMessages(messages),
  stopWhen: isStepCount(5), // Stop after 5 steps with tool calls
});

// Client-side: Configure automatic submission
import { useChat } from '@ai-sdk/react';
import {
  DefaultChatTransport,
  lastAssistantMessageIsCompleteWithToolCalls,
} from 'ai';

const { messages, sendMessage, addToolOutput } = useChat({
  // Automatically submit when all tool results are available
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

  async onToolCall({ toolCall }) {
    const result = await executeToolCall(toolCall);

    // Important: Don't await addToolOutput inside onToolCall to avoid deadlocks
    addToolOutput({
      tool: toolCall.toolName,
      toolCallId: toolCall.toolCallId,
      output: result,
    });
  },
});
중요: `sendAutomaticallyWhen`을 사용할 때는 `onToolCall` 안에서 `addToolOutput`에 `await`를 사용하지 마세요. 데드락이 발생할 수 있어요. 자동 제출을 사용하지 않고 `sendMessage()`를 수동 호출하기 전에 메시지가 업데이트되도록 해야 할 때는 `await`가 유용합니다.

이 변경은 tool 호출 처리에 더 많은 유연성을 제공하고 클라이언트 동작을 서버 측 멀티스텝 실행 패턴과 일치시킵니다.

새 tool 제출 방식에 대한 자세한 내용은 아래의 Tool Result Submission Changes 섹션을 참고하세요.

initialMessages 이름 변경 (Initial Messages Renamed)

initialMessages 옵션이 messages로 이름이 변경됐어요.

import { useChat, type Message } from '@ai-sdk/react';

function ChatComponent({ initialMessages }: { initialMessages: Message[] }) {
  const { messages } = useChat({
    initialMessages: initialMessages,
    // ...
  });

  // your component
}
import { useChat, type UIMessage } from '@ai-sdk/react';

function ChatComponent({ initialMessages }: { initialMessages: UIMessage[] }) {
  const { messages } = useChat({
    messages: initialMessages,
    // ...
  });

  // your component
}

채팅 인스턴스 공유 (Sharing Chat Instances)

v4에서는 여러 useChat 훅에서 같은 id 파라미터를 사용해 컴포넌트 간에 채팅 상태를 공유할 수 있었어요.

// Component A
const { messages } = useChat({
  id: 'shared-chat',
  api: '/api/chat',
});

// Component B - would share the same chat state
const { messages } = useChat({
  id: 'shared-chat',
  api: '/api/chat',
});

v5에서는 공유 Chat 인스턴스를 전달해 채팅 인스턴스를 명시적으로 공유해야 해요.

// e.g. Store Chat instance in React Context and create a custom hook

// Component A
const { chat } = useSharedChat(); // Custom hook that accesses shared Chat from context

const { messages, sendMessage } = useChat({
  chat, // Pass the shared chat instance
});

// Component B - shares the same chat instance
const { chat } = useSharedChat(); // Same hook to access shared Chat from context

const { messages } = useChat({
  chat, // Same shared chat instance
});

컴포넌트 간 채팅 상태 공유에 대한 완전한 예시는 Share Chat State Across Components 레시피를 참고하세요.

채팅 transport 아키텍처 (Chat Transport Architecture)

직접적인 API 옵션 대신 transport 객체로 설정이 처리됩니다.

import { useChat } from '@ai-sdk/react';

const { messages } = useChat({
  api: '/api/chat',
  credentials: 'include',
  headers: { 'Custom-Header': 'value' },
});
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

const { messages } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
    credentials: 'include',
    headers: { 'Custom-Header': 'value' },
  }),
});

관리형 입력 상태 제거 (Removed Managed Input State)

useChat 훅은 더 이상 입력 상태를 내부적으로 관리하지 않아요. 입력 상태를 수동으로 관리해야 합니다.

import { useChat } from '@ai-sdk/react';

export default function Page() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat',
  });

  return (
    <form onSubmit={handleSubmit}>
      <input value={input} onChange={handleInputChange} />
      <button type="submit">Send</button>
    </form>
  );
}
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });

  const handleSubmit = e => {
    e.preventDefault();
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button type="submit">Send</button>
    </form>
  );
}

메시지 전송: append → sendMessage

append 함수가 sendMessage로 대체되고 구조화된 메시지 형식이 필요해졌어요.

const { append } = useChat();

// Simple text message
append({ role: 'user', content: 'Hello' });

// With custom body
append(
  {
    role: 'user',
    content: 'Hello',
  },
  { body: { imageUrl: 'https://...' } },
);
const { sendMessage } = useChat();

// Simple text message (most common usage)
sendMessage({ text: 'Hello' });

// Or with explicit parts array
sendMessage({
  parts: [{ type: 'text', text: 'Hello' }],
});

// With custom body (via request options)
sendMessage(
  { role: 'user', parts: [{ type: 'text', text: 'Hello' }] },
  { body: { imageUrl: 'https://...' } },
);

메시지 재생성: reload → regenerate

reload 함수가 향상된 기능과 함께 regenerate로 이름이 변경됐어요.

const { reload } = useChat();

// Regenerate last message
reload();
const { regenerate } = useChat();

// Regenerate last message
regenerate();

// Regenerate specific message
regenerate({ messageId: 'message-123' });

onResponse 제거

onResponse 콜백이 useChat과 useCompletion에서 제거됐어요.

const { messages } = useChat({
  onResponse(response) {
    // handle response
  },
});
const { messages } = useChat({
  // onResponse is no longer available
});

sendExtraMessageFields 기본화

sendExtraMessageFields 옵션이 제거되고 이제 기본 동작이 됐어요.

const { messages } = useChat({
  sendExtraMessageFields: true,
});
const { messages } = useChat({
  // sendExtraMessageFields is now the default
});

keepLastMessageOnError 제거

더 이상 필요하지 않아 keepLastMessageOnError 옵션이 제거됐어요.

const { messages } = useChat({
  keepLastMessageOnError: true,
});
const { messages } = useChat({
  // keepLastMessageOnError is no longer needed
});

채팅 요청 옵션 변경 사항 (Chat Request Options Changes)

data와 allowEmptySubmit 옵션이 ChatRequestOptions에서 제거됐어요.

handleSubmit(e, {
  data: { imageUrl: 'https://...' },
  body: { custom: 'value' },
  allowEmptySubmit: true,
});
sendMessage(
  {
    /* yourMessage */
  },
  {
    body: {
      custom: 'value',
      imageUrl: 'https://...', // Move data to body
    },
  },
);

요청 옵션 타입 이름 변경 (Request Options Type Rename)

RequestOptions가 CompletionRequestOptions로 이름이 변경됐어요.

import type { RequestOptions } from 'ai';
import type { CompletionRequestOptions } from 'ai';

addToolResult → addToolOutput

addToolResult 메서드가 addToolOutput으로 이름이 변경됐어요. 또한 다른 tool 관련 API와의 일관성을 위해 result 파라미터가 output으로 이름이 변경됐어요.

const { addToolResult } = useChat();

// Add tool result with 'result' parameter
addToolResult({
  toolCallId: 'tool-call-123',
  result: 'Weather: 72°F, sunny',
});
const { addToolOutput } = useChat();

// Add tool output with 'output' parameter and 'tool' name for type safety
addToolOutput({
  tool: 'getWeather',
  toolCallId: 'tool-call-123',
  output: 'Weather: 72°F, sunny',
});
`addToolResult`는 여전히 사용할 수 있지만 deprecated 입니다. 버전 6에서 제거될 예정이에요.

Tool 결과 제출 변경 사항 (Tool Result Submission Changes)

useChat와 Chat 컴포넌트에서 자동 tool 결과 제출 동작이 업데이트됐어요. 이제 tool 결과를 제출하는 시점에 대해 더 많은 제어와 유연성을 갖습니다.

  • onToolCall은 더 이상 tool 결과를 자동 제출하기 위한 값을 반환하는 것을 지원하지 않아요
  • tool 결과를 제공하려면 명시적으로 addToolOutput을 호출해야 해요
  • 자동 제출에는 lastAssistantMessageIsCompleteWithToolCalls 헬퍼와 함께 sendAutomaticallyWhen을 사용하세요
  • 중요: 데드락을 피하려면 onToolCall 안에서 addToolOutput에 await를 사용하지 마세요
  • maxSteps 파라미터가 Chat 컴포넌트와 useChat 훅에서 제거됐어요 (see maxSteps Removal)
  • 멀티스텝 tool 실행에는 서버 측 stopWhen 조건을 대신 사용하세요
const { messages, sendMessage, addToolResult } = useChat({
  maxSteps: 5, // Removed in v5

  // Automatic submission by returning a value
  async onToolCall({ toolCall }) {
    if (toolCall.toolName === 'getLocation') {
      const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco'];
      return cities[Math.floor(Math.random() * cities.length)];
    }
  },
});
import { useChat } from '@ai-sdk/react';
import {
  DefaultChatTransport,
  lastAssistantMessageIsCompleteWithToolCalls,
} from 'ai';

const { messages, sendMessage, addToolOutput } = useChat({
  // Automatic submission with helper
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

  async onToolCall({ toolCall }) {
    if (toolCall.toolName === 'getLocation') {
      const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco'];

      // Important: Don't await inside onToolCall to avoid deadlocks
      addToolOutput({
        tool: 'getLocation',
        toolCallId: toolCall.toolCallId,
        output: cities[Math.floor(Math.random() * cities.length)],
      });
    }
  },
});

로딩 상태 변경 사항 (Loading State Changes)

deprecated 된 isLoading 헬퍼가 status를 위해 제거됐어요.

const { isLoading } = useChat();
const { status } = useChat();
// Use state instead of isLoading for more granular control

스트림 재개 지원 (Resume Stream Support)

재개(resume) 기능이 experimental_resume에서 resumeStream으로 이동했어요.

// Resume was experimental
const { messages } = useChat({
  experimental_resume: true,
});
const { messages } = useChat({
  resumeStream: true, // Resume interrupted streams
});

동적 body 값 (Dynamic Body Values)

v4에서는 useChat 설정의 body 옵션이 컴포넌트 상태 변경에 따라 동적으로 업데이트됐어요. v5에서는 body 값이 첫 렌더링에서만 캡처되고 컴포넌트 수명주기 내내 정적으로 유지됩니다.

const [temperature, setTemperature] = useState(0.7);

const { messages } = useChat({
  api: '/api/chat',
  body: {
    temperature, // This would update dynamically in v4
  },
});
const [temperature, setTemperature] = useState(0.7);

// Option 1: Use request-level configuration (Recommended)
const { messages, sendMessage } = useChat({
  transport: new DefaultChatTransport({ api: '/api/chat' }),
});

// Pass dynamic values at request time
sendMessage(
  { text: input },
  {
    body: {
      temperature, // Current temperature value at request time
    },
  },
);

// Option 2: Use function configuration with useRef
const temperatureRef = useRef(temperature);
temperatureRef.current = temperature;

const { messages } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
    body: () => ({
      temperature: temperatureRef.current,
    }),
  }),
});

요청 구성에 대한 자세한 내용은 Chatbot guide를 참고하세요.

사용량 정보 (Usage Information)

v4에서는 onFinish 콜백의 options 파라미터로 사용량 정보를 직접 접근할 수 있었어요. v5에서는 사용량 데이터가 toUIMessageStreamResponse의 messageMetadata 함수를 사용해 개별 메시지에 메타데이터로 첨부됩니다.

const { messages } = useChat({
  onFinish(message, options) {
    const usage = options.usage;
    console.log('Usage:', usage);
  },
});
import {
  convertToModelMessages,
  streamText,
  UIMessage,
  type LanguageModelUsage,
} from 'ai';
__PROVIDER_IMPORT__;

// Create a new metadata type (optional for type-safety)
type MyMetadata = {
  totalUsage: LanguageModelUsage;
};

// Create a new custom message type with your own metadata
export type MyUIMessage = UIMessage<MyMetadata>;

export async function POST(req: Request) {
  const { messages }: { messages: MyUIMessage[] } = await req.json();

  const result = streamText({
    model: __MODEL__,
    messages: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse({
    originalMessages: messages,
    messageMetadata: ({ part }) => {
      // Send total usage when generation is finished
      if (part.type === 'finish') {
        return { totalUsage: part.totalUsage };
      }
    },
  });
}

그런 다음 클라이언트에서 메시지 수준 메타데이터에 접근할 수 있어요.

'use client';

import { useChat } from '@ai-sdk/react';
import type { MyUIMessage } from './api/chat/route';
import { DefaultChatTransport } from 'ai';

export default function Chat() {
  // Use custom message type defined on the server (optional for type-safety)
  const { messages } = useChat<MyUIMessage>({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(m => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.parts.map(part => {
            if (part.type === 'text') {
              return part.text;
            }
          })}
          {/* Render usage via metadata */}
          {m.metadata?.totalUsage && (
            <div>Total usage: {m.metadata?.totalUsage.totalTokens} tokens</div>
          )}
        </div>
      ))}
    </div>
  );
}

useChat의 onFinish 콜백에서도 메타데이터에 접근할 수 있어요:

'use client';

import { useChat } from '@ai-sdk/react';
import type { MyUIMessage } from './api/chat/route';
import { DefaultChatTransport } from 'ai';

export default function Chat() {
  // Use custom message type defined on the server (optional for type-safety)
  const { messages } = useChat<MyUIMessage>({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
    onFinish: ({ message }) => {
      // Access message metadata via onFinish callback
      console.log(message.metadata?.totalUsage);
    },
  });
}

요청 본문 준비: experimental_prepareRequestBody → prepareSendMessagesRequest

experimental_prepareRequestBody 옵션이 transport 설정의 prepareSendMessagesRequest로 대체됐어요.

import { useChat } from '@ai-sdk/react';

const { messages } = useChat({
  api: '/api/chat',
  // Only send the last message to the server:
  experimental_prepareRequestBody({ messages, id }) {
    return { message: messages[messages.length - 1], id };
  },
});
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

const { messages } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
    // Only send the last message to the server:
    prepareSendMessagesRequest({ messages, id }) {
      return { body: { message: messages[messages.length - 1], id } };
    },
  }),
});

@ai-sdk/vue 변경 사항

Vue.js 통합이 완전히 재구성되며 useChat 컴포저블이 Chat 클래스로 대체됐어요.

useChat → Chat 클래스

<script setup>
import { useChat } from '@ai-sdk/vue';

const { messages, input, handleSubmit } = useChat({
  api: '/api/chat',
});
</script>
<script setup>
import { Chat } from '@ai-sdk/vue';
import { DefaultChatTransport } from 'ai';
import { ref } from 'vue';

const input = ref('');
const chat = new Chat({
  transport: new DefaultChatTransport({ api: '/api/chat' }),
});

const handleSubmit = (e: Event) => {
  e.preventDefault();
  chat.sendMessage({ text: input.value });
  input.value = '';
};
</script>

메시지 구조 변경 사항 (Message Structure Changes)

메시지는 이제 content 문자열 대신 parts 배열을 사용해요.

<template>
  <div v-for="message in messages" :key="message.id">
    <div>{{ message.role }}: {{ message.content }}</div>
  </div>
</template>
<template>
  <div v-for="message in chat.messages" :key="message.id">
    <div>{{ message.role }}:</div>
    <div v-for="part in message.parts" :key="part.type">
      <span v-if="part.type === 'text'">{{ part.text }}</span>
    </div>
  </div>
</template>

@ai-sdk/svelte 변경 사항

Svelte 통합도 새 생성자 패턴과 readonly 속성으로 업데이트됐어요.

생성자 API 변경 사항 (Constructor API Changes)

import { Chat } from '@ai-sdk/svelte';

const chatInstance = Chat({
  api: '/api/chat',
});
import { Chat } from '@ai-sdk/svelte';
import { DefaultChatTransport } from 'ai';

const chatInstance = Chat(() => ({
  transport: new DefaultChatTransport({ api: '/api/chat' }),
}));
속성이 readonly가 됨 (Properties Made Readonly)

속성은 이제 readonly이며 setter 메서드로 업데이트해야 해요.

// Direct property mutation was allowed
chatInstance.messages = [...chatInstance.messages, newMessage];
// Must use setter methods
chatInstance.setMessages([...chatInstance.messages, newMessage]);
관리형 입력 제거 (Removed Managed Input)

React 및 Vue와 마찬가지로 Svelte 통합에서 입력 관리가 제거됐어요.

// Input was managed internally
const { messages, input, handleSubmit } = chatInstance;
// Must manage input state manually
let input = '';
const { messages, sendMessage } = chatInstance;

const handleSubmit = () => {
  sendMessage({ text: input });
  input = '';
};

@ai-sdk/ui-utils 패키지 제거 (Package Removal)

@ai-sdk/ui-utils 패키지가 제거되고 그 export가 메인 ai 패키지로 이동했어요.

import { getTextFromDataUrl } from '@ai-sdk/ui-utils';
import { getTextFromDataUrl } from 'ai';

참고: processDataStream은 v5.0에서 완전히 제거됐어요. UI 메시지 스트림 처리에는 readUIMessageStream을, 대부분의 사용 사례에는 더 설정이 가능한 Chat/useChat API를 사용하세요.

useCompletion 변경 사항

useCompletion 훅에서 data 속성이 제거됐어요.

const {
  completion,
  handleSubmit,
  data, // No longer available
} = useCompletion();
const {
  completion,
  handleSubmit,
  // data property removed entirely
} = useCompletion();

useAssistant 제거

useAssistant 훅이 제거됐어요.

import { useAssistant } from '@ai-sdk/react';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

function Chat() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });

  // ...
}

useAssistant 훅은 OpenAI Assistants API에 특화되어 있었어요. OpenAI는 해당 API를 Responses API를 위해 deprecated 했습니다. 위와 같이 useChat을 라우트용으로 구성한 다음, 라우트에서 UI 메시지 스트림을 반환하세요:

import { openai } from '@ai-sdk/openai';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai.responses('gpt-4o-mini'),
    prompt: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}

영속적인 대화 상태와 내장 tool이 필요하다면 OpenAI Responses API guide를 참고하세요.

useChat를 다른 백엔드에 연결해야 한다면 transport documentation과 stream protocol을, 기존 OpenAI Assistants 데이터와 API 호출 마이그레이션은 OpenAI의 Assistants migration guide를 참고하세요.

첨부 파일 → File Parts (Attachments → File Parts)

experimental_attachments 속성이 parts 배열로 대체됐어요.

{
  messages.map(message => (
    <div className="flex flex-col gap-2">
      {message.content}

      <div className="flex flex-row gap-2">
        {message.experimental_attachments?.map((attachment, index) =>
          attachment.contentType?.includes('image/') ? (
            <img src={attachment.url} alt={attachment.name} />
          ) : attachment.contentType?.includes('text/') ? (
            <div className="w-32 h-24 p-2 overflow-hidden text-xs border rounded-md ellipsis text-zinc-500">
              {getTextFromDataUrl(attachment.url)}
            </div>
          ) : null,
        )}
      </div>
    </div>
  ));
}
{
  messages.map(message => (
    <div>
      {message.parts.map((part, index) => {
        if (part.type === 'text') {
          return <div key={index}>{part.text}</div>;
        }

        if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
          return (
            <div key={index}>
              <img src={part.url} />
            </div>
          );
        }
      })}
    </div>
  ));
}
일부 모델은 텍스트 파일(text/plain, text/markdown, text/csv 등)을 file part로 지원하지 않아요. 텍스트 파일의 경우 컨텍스트를 텍스트 part로 읽어서 보내는 방법을 사용하세요:
// Instead of this:
{ type: 'file', data: buffer, mediaType: 'text/plain' }

// Do this:
{ type: 'text', text: buffer.toString('utf-8') }

임베딩 변경 사항 (Embedding Changes)

임베딩용 Provider 옵션 (Provider Options for Embeddings)

임베딩 모델 설정이 이제 모델 파라미터 대신 provider 옵션을 사용해요.

const { embedding } = await embed({
  model: openai('text-embedding-3-small', {
    dimensions: 10,
  }),
});
const { embedding } = await embed({
  model: openai('text-embedding-3-small'),
  providerOptions: {
    openai: {
      dimensions: 10,
    },
  },
});

rawResponse → response

rawResponse 속성이 response로 이름이 변경됐어요.

const { rawResponse } = await embed(/* */);
const { response } = await embed(/* */);

embedMany의 병렬 요청 (Parallel Requests in embedMany)

embedMany는 이제 설정 가능한 maxParallelCalls 옵션으로 병렬 요청을 합니다.

const { embeddings, usage } = await embedMany({
  maxParallelCalls: 2, // Limit parallel requests
  model: 'openai/text-embedding-3-small',
  values: [
    'sunny day at the beach',
    'rainy afternoon in the city',
    'snowy night in the mountains',
  ],
});

LangChain 어댑터가 @ai-sdk/langchain으로 이동

LangChainAdapter가 @ai-sdk/langchain으로 이동하고 API가 UI 메시지 스트림을 사용하도록 업데이트됐어요.

import { LangChainAdapter } from 'ai';

const response = LangChainAdapter.toDataStreamResponse(stream);
import { toUIMessageStream } from '@ai-sdk/langchain';
import { createUIMessageStreamResponse } from 'ai';

const response = createUIMessageStreamResponse({
  stream: toUIMessageStream(stream),
});
새 패키지 설치를 잊지 마세요: `npm install @ai-sdk/langchain`

LlamaIndex 어댑터가 @ai-sdk/llamaindex으로 이동

LlamaIndexAdapter가 @ai-sdk/llamaindex라는 별도 패키지로 분리되고 같은 UI 메시지 스트림 패턴을 따릅니다.

import { LlamaIndexAdapter } from 'ai';

const response = LlamaIndexAdapter.toDataStreamResponse(stream);
import { toUIMessageStream } from '@ai-sdk/llamaindex';
import { createUIMessageStreamResponse } from 'ai';

const response = createUIMessageStreamResponse({
  stream: toUIMessageStream(stream),
});
새 패키지 설치를 잊지 마세요: `npm install @ai-sdk/llamaindex`

스트리밍 아키텍처 (Streaming Architecture)

스트리밍 아키텍처가 v5에서 콘텐츠 구분 개선, 여러 part의 동시 스트리밍, 개선된 실시간 UX를 지원하도록 완전히 재설계됐어요.

스트림 프로토콜 변경 사항 (Stream Protocol Changes)

스트림 프로토콜: 단일 chunk → start/delta/end 패턴

기본 스트리밍 패턴이 단일 chunk에서 각 콘텐츠 블록에 고유 ID가 있는 3단계 패턴으로 변경됐어요.

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'text-delta': {
      process.stdout.write(chunk.textDelta);
      break;
    }
  }
}
for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'text-start': {
      // New: Initialize a text block with unique ID
      console.log(`Starting text block: ${chunk.id}`);
      break;
    }
    case 'text-delta': {
      // Changed: Now includes ID and uses 'delta' property
      process.stdout.write(chunk.delta); // Changed from 'textDelta'
      break;
    }
    case 'text-end': {
      // New: Finalize the text block
      console.log(`Completed text block: ${chunk.id}`);
      break;
    }
  }
}

Reasoning 스트리밍 패턴

Reasoning 콘텐츠도 이제 같은 start/delta/end 패턴을 따릅니다:

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'reasoning': {
      // Single chunk with full reasoning text
      console.log('Reasoning:', chunk.text);
      break;
    }
  }
}
for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'reasoning-start': {
      console.log(`Starting reasoning block: ${chunk.id}`);
      break;
    }
    case 'reasoning-delta': {
      process.stdout.write(chunk.delta);
      break;
    }
    case 'reasoning-end': {
      console.log(`Completed reasoning block: ${chunk.id}`);
      break;
    }
  }
}

Tool 입력 스트리밍

Tool 입력이 생성되는 동안 스트리밍될 수 있어요:

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'tool-input-start': {
      console.log(`Starting tool input for ${chunk.toolName}: ${chunk.id}`);
      break;
    }
    case 'tool-input-delta': {
      // Stream the JSON input as it's being generated
      process.stdout.write(chunk.delta);
      break;
    }
    case 'tool-input-end': {
      console.log(`Completed tool input: ${chunk.id}`);
      break;
    }
    case 'tool-call': {
      // Final tool call with complete input
      console.log('Tool call:', chunk.toolName, chunk.input);
      break;
    }
  }
}

onChunk 콜백 변경 사항

onChunk 콜백은 이제 ID와 start/delta/end 패턴이 있는 새 스트리밍 chunk 타입을 받습니다.

const result = streamText({
  model: __MODEL__,
  prompt: 'Write a story',
  onChunk({ chunk }) {
    switch (chunk.type) {
      case 'text-delta': {
        // Single property with text content
        console.log('Text delta:', chunk.textDelta);
        break;
      }
    }
  },
});
const result = streamText({
  model: __MODEL__,
  prompt: 'Write a story',
  onChunk({ chunk }) {
    switch (chunk.type) {
      case 'text-delta': {
        // Text chunks now use single 'text' type
        console.log('Text chunk:', chunk.text);
        break;
      }
      case 'reasoning': {
        // Reasoning chunks use single 'reasoning' type
        console.log('Reasoning chunk:', chunk.text);
        break;
      }
      case 'source': {
        console.log('Source chunk:', chunk);
        break;
      }
      case 'tool-call': {
        console.log('Tool call:', chunk.toolName, chunk.input);
        break;
      }
      case 'tool-input-start': {
        console.log(
          `Tool input started for ${chunk.toolName}:`,
          chunk.toolCallId,
        );
        break;
      }
      case 'tool-input-delta': {
        console.log(`Tool input delta for ${chunk.toolCallId}:`, chunk.delta);
        break;
      }
      case 'tool-result': {
        console.log('Tool result:', chunk.output);
        break;
      }
      case 'raw': {
        console.log('Raw chunk:', chunk);
        break;
      }
    }
  },
});

파일 스트림 part 재구성 (File Stream Parts Restructure)

스트림의 파일 part가 평탄화(flatten)됐어요.

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'file': {
      console.log('Media type:', chunk.file.mediaType);
      console.log('File data:', chunk.file.data);
      break;
    }
  }
}
for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'file': {
      console.log('Media type:', chunk.mediaType);
      console.log('File data:', chunk.data);
      break;
    }
  }
}

소스 스트림 part 재구성 (Source Stream Parts Restructure)

소스 스트림 part가 평탄화(flatten)됐어요.

for await (const part of result.fullStream) {
  if (part.type === 'source' && part.source.sourceType === 'url') {
    console.log('ID:', part.source.id);
    console.log('Title:', part.source.title);
    console.log('URL:', part.source.url);
  }
}
for await (const part of result.fullStream) {
  if (part.type === 'source' && part.sourceType === 'url') {
    console.log('ID:', part.id);
    console.log('Title:', part.title);
    console.log('URL:', part.url);
  }
}

finish 이벤트 변경 사항

스트림 finish 이벤트가 일관성을 위해 이름이 변경됐어요.

for await (const part of result.fullStream) {
  switch (part.type) {
    case 'step-finish': {
      console.log('Step finished:', part.finishReason);
      break;
    }
    case 'finish': {
      console.log('Usage:', part.usage);
      break;
    }
  }
}
for await (const part of result.fullStream) {
  switch (part.type) {
    case 'finish-step': {
      // Renamed from 'step-finish'
      console.log('Step finished:', part.finishReason);
      break;
    }
    case 'finish': {
      console.log('Total Usage:', part.totalUsage); // Changed from 'usage'
      break;
    }
  }
}

스트림 프로토콜 변경 사항

독점 프로토콜 → Server-Sent Events

데이터 스트림 프로토콜이 Server-Sent Events를 사용하도록 업데이트됐어요.

import { createDataStream, formatDataStreamPart } from 'ai';

const dataStream = createDataStream({
  execute: writer => {
    writer.writeData('initialized call');
    writer.write(formatDataStreamPart('text', 'Hello'));
    writer.writeSource({
      type: 'source',
      sourceType: 'url',
      id: 'source-1',
      url: 'https://example.com',
      title: 'Example Source',
    });
  },
});
import { createUIMessageStream } from 'ai';

const stream = createUIMessageStream({
  execute: ({ writer }) => {
    writer.write({ type: 'data', value: ['initialized call'] });
    writer.write({ type: 'text', value: 'Hello' });
    writer.write({
      type: 'source-url',
      value: {
        type: 'source',
        id: 'source-1',
        url: 'https://example.com',
        title: 'Example Source',
      },
    });
  },
});

데이터 스트림 응답 헬퍼 함수 이름 변경

스트리밍 API가 데이터 스트림에서 UI 메시지 스트림으로 완전히 재구성됐어요.

// Express/Node.js servers
app.post('/stream', async (req, res) => {
  const result = streamText({
    model: __MODEL__,
    prompt: 'Generate content',
  });

  result.pipeDataStreamToResponse(res);
});

// Next.js API routes
const result = streamText({
  model: __MODEL__,
  prompt: 'Generate content',
});

return result.toDataStreamResponse();
// Express/Node.js servers
app.post('/stream', async (req, res) => {
  const result = streamText({
    model: __MODEL__,
    prompt: 'Generate content',
  });

  result.pipeUIMessageStreamToResponse(res);
});

// Next.js API routes
const result = streamText({
  model: __MODEL__,
  prompt: 'Generate content',
});

return result.toUIMessageStreamResponse();

스트림 변환 함수 이름 변경

여러 스트림 관련 함수가 일관성을 위해 이름이 변경됐어요.

import { DataStreamToSSETransformStream } from 'ai';
import { JsonToSseTransformStream } from 'ai';

오류 처리: getErrorMessage → onError

toDataStreamResponse의 getErrorMessage 옵션이 toUIMessageStreamResponse의 onError로 대체되어, 클라이언트로의 오류 전달을 더 세밀하게 제어할 수 있어요.

기본적으로 민감한 정보 누출을 막기 위해 오류 메시지는 클라이언트로 전송되지 않습니다. onError 콜백을 사용하면 어떤 오류 정보를 클라이언트로 전달할지 명시적으로 제어할 수 있어요.

return result.toDataStreamResponse({
  getErrorMessage: error => {
    // Return sanitized error data to send to client
    // Only return what you want the client to see!
    return {
      errorCode: 'STREAM_ERROR',
      message: 'An error occurred while processing your request',
      // In production, avoid sending error.message directly to prevent information leakage
    };
  },
});
return result.toUIMessageStreamResponse({
  onError: error => {
    // Return sanitized error data to send to client
    // Only return what you want the client to see!
    return {
      errorCode: 'STREAM_ERROR',
      message: 'An error occurred while processing your request',
      // In production, avoid sending error.message directly to prevent information leakage
    };
  },
});

유틸리티 변경 사항

ID 생성 변경 사항 (ID Generation Changes)

createIdGenerator() 함수가 이제 size 인자를 요구합니다.

const generator = createIdGenerator({ prefix: 'msg' });
const id = generator(16); // Custom size at call time
const generator = createIdGenerator({ prefix: 'msg', size: 16 });
const id = generator(); // Fixed size from creation

IDGenerator → IdGenerator

타입 이름이 업데이트됐어요.

import { IDGenerator } from 'ai';
import { IdGenerator } from 'ai';

Provider 인터페이스 변경 사항

LanguageModel V2 import

LanguageModelV3는 이제 @ai-sdk/provider에서 import 해야 해요.

import { LanguageModelV3 } from 'ai';
import { LanguageModelV3 } from '@ai-sdk/provider';

미들웨어 이름 변경 (Middleware Rename)

LanguageModelV1Middleware가 이름이 변경되고 이동했어요.

import { LanguageModelV1Middleware } from 'ai';
import { LanguageModelV3Middleware } from '@ai-sdk/provider';

usage 토큰 속성

토큰 사용량 속성이 일관성을 위해 이름이 변경됐어요.

// In language model implementations
{
  usage: {
    promptTokens: 10,
    completionTokens: 20
  }
}
// In language model implementations
{
  usage: {
    inputTokens: 10,
    outputTokens: 20,
    totalTokens: 30 // Now required
  }
}

스트림 part 타입 변경 사항

LanguageModelV3StreamPart 타입이 start/delta/end 패턴과 ID를 가진 새 스트리밍 아키텍처를 지원하도록 확장됐어요.

// V4: Simple stream parts
type LanguageModelV3StreamPart =
  | { type: 'text-delta'; textDelta: string }
  | { type: 'reasoning'; text: string }
  | { type: 'tool-call'; toolCallId: string; toolName: string; input: string };
// V5: Enhanced stream parts with IDs and lifecycle events
type LanguageModelV3StreamPart =
  // Text blocks with start/delta/end pattern
  | {
      type: 'text-start';
      id: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'text-delta';
      id: string;
      delta: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'text-end';
      id: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }

  // Reasoning blocks with start/delta/end pattern
  | {
      type: 'reasoning-start';
      id: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'reasoning-delta';
      id: string;
      delta: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'reasoning-end';
      id: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }

  // Tool input streaming
  | {
      type: 'tool-input-start';
      id: string;
      toolName: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'tool-input-delta';
      id: string;
      delta: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }
  | {
      type: 'tool-input-end';
      id: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }

  // Enhanced tool calls
  | {
      type: 'tool-call';
      toolCallId: string;
      toolName: string;
      input: string;
      providerMetadata?: SharedV2ProviderMetadata;
    }

  // Stream lifecycle events
  | { type: 'stream-start'; warnings: Array<SharedV3Warning> }
  | {
      type: 'finish';
      usage: LanguageModelV3Usage;
      finishReason: LanguageModelV3FinishReason;
      providerMetadata?: SharedV2ProviderMetadata;
    };

rawResponse → response

Provider 응답 객체가 업데이트됐어요.

// In language model implementations
{
  rawResponse: {
    /* ... */
  }
}
// In language model implementations
{
  response: {
    /* ... */
  }
}

wrapLanguageModel 이제 안정적 (now stable)

import { experimental_wrapLanguageModel } from 'ai';
import { wrapLanguageModel } from 'ai';

activeTools 더 이상 experimental 아님

const result = await generateText({
  model: __MODEL__,
  messages,
  tools: { weatherTool, locationTool },
  experimental_activeTools: ['weatherTool'],
});
const result = await generateText({
  model: __MODEL__,
  messages,
  tools: { weatherTool, locationTool },
  activeTools: ['weatherTool'], // No longer experimental
});

prepareStep 더 이상 experimental 아님

experimental_prepareStep 옵션이 정식 기능으로 승격되어 experimental 접두사가 더 이상 필요 없어요.

const result = await generateText({
  model: __MODEL__,
  messages,
  tools: { weatherTool, locationTool },
  experimental_prepareStep: ({ steps, stepNumber, model }) => {
    console.log('Preparing step:', stepNumber);
    return {
      activeTools: ['weatherTool'],
      system: 'Be helpful and concise.',
    };
  },
});
const result = await generateText({
  model: __MODEL__,
  messages,
  tools: { weatherTool, locationTool },
  prepareStep: ({ steps, stepNumber, model }) => {
    console.log('Preparing step:', stepNumber);
    return {
      activeTools: ['weatherTool'],
      system: 'Be helpful and concise.',
      // Can also configure toolChoice, model, etc.
    };
  },
});

prepareStep 함수는 { steps, stepNumber, model }을 받고 다음을 반환할 수 있어요:

  • model: 이 step에 사용할 다른 모델
  • activeTools: 사용 가능하게 만들 tool 목록
  • toolChoice: Tool 선택 전략
  • system: 이 step의 시스템 메시지
  • undefined: 기본 설정 사용

temperature 기본값 제거 (Temperature Default Removal)

Temperature가 더 이상 기본적으로 0으로 설정되지 않아요.

await generateText({
  model: __MODEL__,
  prompt: 'Write a creative story',
  // Implicitly temperature: 0
});
await generateText({
  model: __MODEL__,
  prompt: 'Write a creative story',
  temperature: 0, // Must explicitly set
});

메시지 영속화 변경 사항 (Message Persistence Changes)

데이터베이스에 메시지를 영속화했다면, 저장된 메시지 데이터를 v5 형식으로 마이그레이션하는 종합적인 안내는 [Data Migration Guide](/docs/migration-guides/migration-guide-5-0-data)를 참고하세요.

v4에서는 일반적으로 streamText의 onFinish 콜백에서 appendResponseMessages나 appendClientMessage 같은 헬퍼 함수로 메시지를 형식화했어요:

import {
  streamText,
  convertToModelMessages,
  appendClientMessage,
  appendResponseMessages,
} from 'ai';

const updatedMessages = appendClientMessage({
  messages,
  message: lastUserMessage,
});

const result = streamText({
  model: __MODEL__,
  messages: updatedMessages,
  experimental_generateMessageId: () => generateId(), // ID generation on streamText
  onFinish: async ({ responseMessages, usage }) => {
    // Use helper functions to format messages
    const finalMessages = appendResponseMessages({
      messages: updatedMessages,
      responseMessages,
    });

    // Save formatted messages to database
    await saveMessages(finalMessages);
  },
});

v5에서는 메시지 영속화가 toUIMessageStreamResponse 메서드를 통해 처리되며, 응답 메시지를 UIMessage 형식으로 자동 형식화합니다:

import { streamText, convertToModelMessages, UIMessage } from 'ai';

const messages: UIMessage[] = [
  // Your existing messages in UIMessage format
];

const result = streamText({
  model: __MODEL__,
  messages: convertToModelMessages(messages),
  // experimental_generateMessageId removed from here
});

return result.toUIMessageStreamResponse({
  originalMessages: messages, // IMPORTANT: Required to prevent duplicate messages
  generateMessageId: () => generateId(), // IMPORTANT: Required for proper message ID generation
  onFinish: ({ messages, responseMessage }) => {
    // messages contains all messages (original + response) in UIMessage format
    saveChat({ chatId, messages });

    // responseMessage contains just the generated message in UIMessage format
    saveMessage({ chatId, message: responseMessage });
  },
});
**중요:** `toUIMessageStreamResponse`를 사용할 때는 항상 `originalMessages`와 `generateMessageId` 파라미터를 모두 제공해야 해요. 이 파라미터가 없으면 UI에서 중복되거나 반복되는 어시스턴트 메시지가 발생할 수 있습니다. 자세한 내용은 [Troubleshooting: Repeated Assistant Messages](/docs/troubleshooting/repeated-assistant-messages)를 참고하세요.

메시지 ID 생성 (Message ID Generation)

experimental_generateMessageId 옵션이 streamText 설정에서 toUIMessageStreamResponse로 이동했어요. ModelMessage보다는 UIMessage와 함께 사용하도록 설계되었기 때문입니다.

const result = streamText({
  model: __MODEL__,
  messages,
  experimental_generateMessageId: () => generateId(),
});
const result = streamText({
  model: __MODEL__,
  messages: convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse({
  generateMessageId: () => generateId(), // No longer experimental
  // ...
});

메시지 ID와 영속화에 대한 자세한 내용은 Chatbot Message Persistence guide를 참고하세요.

createUIMessageStream 사용하기

더 복잡한 시나리오, 특히 데이터 part를 다룰 때는 createUIMessageStream을 사용할 수 있어요:

import {
  createUIMessageStream,
  createUIMessageStreamResponse,
  streamText,
  convertToModelMessages,
  UIMessage,
} from 'ai';

const stream = createUIMessageStream({
  originalMessages: messages,
  generateId: generateId, // Required for proper message ID generation
  execute: ({ writer }) => {
    // Write custom data parts
    writer.write({
      type: 'data',
      data: { status: 'processing', timestamp: Date.now() },
    });

    // Stream the AI response
    const result = streamText({
      model: __MODEL__,
      messages: convertToModelMessages(messages),
    });

    writer.merge(result.toUIMessageStream());
  },
  onFinish: ({ messages }) => {
    // messages contains all messages (original + response + data parts) in UIMessage format
    saveChat({ chatId, messages });
  },
});

return createUIMessageStreamResponse({ stream });

Provider 및 모델 변경 사항 (Provider & Model Changes)

OpenAI

기본 Provider 인스턴스가 Responses API 사용

AI SDK 5에서 기본 OpenAI provider 인스턴스는 Responses API를 사용하고, AI SDK 4는 Chat Completions API를 사용했어요. Chat Completions API는 계속 완전히 지원되며 openai.chat(...)로 사용할 수 있습니다.

import { openai } from '@ai-sdk/openai';

const defaultModel = openai('gpt-4.1-mini'); // Chat Completions API
import { openai } from '@ai-sdk/openai';

const defaultModel = openai('gpt-4.1-mini'); // Responses API

// Specify a specific API when needed:
const chatCompletionsModel = openai.chat('gpt-4.1-mini');
const responsesModel = openai.responses('gpt-4.1-mini');
Responses와 Chat Completions API는 서로 다른 동작과 기본값을 가집니다. Chat Completions API에 의존하고 있다면 모델 인스턴스를 `openai.chat(...)`으로 전환하고 설정을 감사(audit)하세요.

Responses API의 Strict 스키마 (strictSchemas)

AI SDK 4.0에서는 Responses 모델에 strictSchemas 옵션(기본값 true)을 설정할 수 있었어요. 이 옵션은 AI SDK 5.0에서 strictJsonSchema로 이름이 바뀌고 기본값이 false가 됐어요.

import { z } from 'zod';
import { generateObject } from 'ai';
import { openai, type OpenAIResponsesProviderOptions } from '@ai-sdk/openai';

const result = await generateObject({
  model: openai.responses('gpt-4.1'),
  schema: z.object({
    // ...
  }),
  providerOptions: {
    openai: {
      strictSchemas: true, // default behavior in AI SDK 4
    } satisfies OpenAIResponsesProviderOptions,
  },
});
import { z } from 'zod';
import { generateObject } from 'ai';
import { openai, type OpenAIResponsesProviderOptions } from '@ai-sdk/openai';

const result = await generateObject({
  model: openai('gpt-4.1-2024'), // uses Responses API
  schema: z.object({
    // ...
  }),
  providerOptions: {
    openai: {
      strictJsonSchema: true, // defaults to false, opt back in to the AI SDK 4 strict behavior
    } satisfies OpenAIResponsesProviderOptions,
  },
});

Chat Completions API를 직접 사용하기 위해 openai.chat(...)을 호출한다면 OpenAIChatLanguageModelOptions로 타입을 지정할 수 있어요. AI SDK 5는 여기에도 같은 strictJsonSchema 옵션을 추가합니다.

구조화된 출력 (Structured Outputs)

structuredOutputs 옵션은 이제 모델 인스턴스 설정이 아니라 provider 옵션으로 설정됩니다.

import { z } from 'zod';
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';

const result = await generateObject({
  model: openai('gpt-4.1', { structuredOutputs: true }), // use Chat Completions API
  schema: z.object({ name: z.string() }),
});
import { z } from 'zod';
import { generateObject } from 'ai';
import { openai, type OpenAIChatLanguageModelOptions } from '@ai-sdk/openai';

const result = await generateObject({
  model: openai.chat('gpt-4.1'), // use Chat Completions API
  schema: z.object({ name: z.string() }),
  providerOptions: {
    openai: {
      structuredOutputs: true,
    } satisfies OpenAIChatLanguageModelOptions,
  },
});

호환성 옵션 제거 (Compatibility Option Removal)

compatibility 옵션이 제거됐어요. 이제 strict 호환성 모드가 기본입니다.

const openai = createOpenAI({
  compatibility: 'strict',
});
const openai = createOpenAI({
  // strict compatibility is now the default
});

레거시 함수 호출 제거 (Legacy Function Calls Removal)

useLegacyFunctionCalls 옵션이 제거됐어요.

const result = streamText({
  model: openai('gpt-4.1', { useLegacyFunctionCalls: true }),
});
const result = streamText({
  model: openai('gpt-4.1'),
});

스트리밍 시뮬레이션 (Simulate Streaming)

simulateStreaming 모델 옵션이 미들웨어로 대체됐어요.

const result = generateText({
  model: openai('gpt-4.1', { simulateStreaming: true }),
  prompt: 'Hello, world!',
});
import { simulateStreamingMiddleware, wrapLanguageModel } from 'ai';

const model = wrapLanguageModel({
  model: openai('gpt-4.1'),
  middleware: simulateStreamingMiddleware(),
});

const result = generateText({
  model,
  prompt: 'Hello, world!',
});

Google

검색 접지(Search Grounding)가 provider 정의 tool이 됨

Search Grounding이 이제 "Google Search"라고 불리며 provider 정의 tool이 됐어요.

const { text, providerMetadata } = await generateText({
  model: google('gemini-1.5-pro', {
    useSearchGrounding: true,
  }),
  prompt: 'List the top 5 San Francisco news from the past week.',
});
import { google } from '@ai-sdk/google';
const { text, sources, providerMetadata } = await generateText({
  model: google('gemini-1.5-pro'),
  prompt:
    'List the top 5 San Francisco news from the past week.'
  tools: {
    google_search: google.tools.googleSearch({}),
  },
});

Amazon Bedrock

Snake case → Camel case

Provider 옵션이 camelCase를 사용하도록 업데이트됐어요.

const result = await generateText({
  model: bedrock('amazon.titan-tg1-large'),
  prompt: 'Hello, world!',
  providerOptions: {
    bedrock: {
      reasoning_config: {
        /* ... */
      },
    },
  },
});
const result = await generateText({
  model: bedrock('amazon.titan-tg1-large'),
  prompt: 'Hello, world!',
  providerOptions: {
    bedrock: {
      reasoningConfig: {
        /* ... */
      },
    },
  },
});

Provider-Utils 변경 사항

deprecated 된 CoreTool* 타입이 제거됐어요.

import {
  CoreToolCall,
  CoreToolResult,
  CoreToolResultUnion,
  CoreToolCallUnion,
  CoreToolChoice,
} from '@ai-sdk/provider-utils';
import {
  ToolCall,
  ToolResult,
  TypedToolResult,
  TypedToolCall,
  ToolChoice,
} from '@ai-sdk/provider-utils';

문제 해결 (Troubleshooting)

Zod와 함께하는 TypeScript 성능 문제

Zod를 AI SDK 5.0과 함께 사용할 때 TypeScript 서버 크래시, 느린 타입 검사, 또는 "Type instantiation is excessively deep and possibly infinite" 같은 오류가 발생하면:

  1. 먼저 Zod 4.1.8 이상을 사용하고 있는지 확인하세요 - 이 버전은 TypeScript 성능 문제를 유발하는 모듈 해석 문제를 수정합니다.

  2. 문제가 지속되면 tsconfig.json을 moduleResolution: "nodenext"로 업데이트하세요:

{
  "compilerOptions": {
    "moduleResolution": "nodenext"
    // ... other options
  }
}

이렇게 하면 표준 Zod import를 계속 사용하면서 TypeScript 성능 문제를 해결할 수 있어요. 문제가 해결되지 않으면 버전별 import 경로를 대안으로 시도해볼 수 있습니다. 자세한 문제 해결 단계는 TypeScript performance issues with Zod를 참고하세요.

Codemod 표 (Codemod Table)

다음 표는 AI SDK 5.0 업그레이드 과정에서 사용 가능한 codemod 목록을 보여줍니다. 자세한 내용은 Codemods 섹션을 참고하세요.

변경 사항 Codemod
AI SDK Core 변경 사항
Flatten streamText file properties v5/flatten-streamtext-file-properties
ID Generation Changes v5/require-createIdGenerator-size-argument
IDGenerator → IdGenerator v5/rename-IDGenerator-to-IdGenerator
Import LanguageModelV3 from provider package v5/import-LanguageModelV3-from-provider-package
Migrate to data stream protocol v2 v5/migrate-to-data-stream-protocol-v2
Move image model maxImagesPerCall v5/move-image-model-maxImagesPerCall
Move LangChain adapter v5/move-langchain-adapter
Move maxSteps to stopWhen v5/move-maxsteps-to-stopwhen
Move provider options v5/move-provider-options
Move React to AI SDK v5/move-react-to-ai-sdk
Move UI utils to AI v5/move-ui-utils-to-ai
Remove experimental wrap language model v5/remove-experimental-wrap-language-model
Remove experimental activeTools v5/remove-experimental-activetools
Remove experimental prepareStep v5/remove-experimental-preparestep
Remove experimental continueSteps v5/remove-experimental-continuesteps
Remove experimental temperature v5/remove-experimental-temperature
Remove experimental truncate v5/remove-experimental-truncate
Remove experimental OpenAI compatibility v5/remove-experimental-openai-compatibility
Remove experimental OpenAI legacy function calls v5/remove-experimental-openai-legacy-function-calls
Remove experimental OpenAI structured outputs v5/remove-experimental-openai-structured-outputs
Remove experimental OpenAI store v5/remove-experimental-openai-store
Remove experimental OpenAI user v5/remove-experimental-openai-user
Remove experimental OpenAI parallel tool calls v5/remove-experimental-openai-parallel-tool-calls
Remove experimental OpenAI response format v5/remove-experimental-openai-response-format
Remove experimental OpenAI logit bias v5/remove-experimental-openai-logit-bias
Remove experimental OpenAI logprobs v5/remove-experimental-openai-logprobs
Remove experimental OpenAI seed v5/remove-experimental-openai-seed
Remove experimental OpenAI service tier v5/remove-experimental-openai-service-tier
Remove experimental OpenAI top logprobs v5/remove-experimental-openai-top-logprobs
Remove experimental OpenAI transform v5/remove-experimental-openai-transform
Remove experimental OpenAI stream options v5/remove-experimental-openai-stream-options
Remove experimental OpenAI prediction v5/remove-experimental-openai-prediction
Remove experimental Anthropic caching v5/remove-experimental-anthropic-caching
Remove experimental Anthropic computer use v5/remove-experimental-anthropic-computer-use
Remove experimental Anthropic PDF support v5/remove-experimental-anthropic-pdf-support
Remove experimental Anthropic prompt caching v5/remove-experimental-anthropic-prompt-caching
Remove experimental Google search grounding v5/remove-experimental-google-search-grounding
Remove experimental Google code execution v5/remove-experimental-google-code-execution
Remove experimental Google cached content v5/remove-experimental-google-cached-content
Remove experimental Google custom headers v5/remove-experimental-google-custom-headers
Rename format stream part v5/rename-format-stream-part
Rename parse stream part v5/rename-parse-stream-part
Replace image type with file type v5/replace-image-type-with-file-type
Replace LlamaIndex adapter v5/replace-llamaindex-adapter
Replace onCompletion with onFinal v5/replace-oncompletion-with-onfinal
Replace provider metadata with provider options v5/replace-provider-metadata-with-provider-options
Replace rawResponse with response v5/replace-rawresponse-with-response
Replace redacted reasoning type v5/replace-redacted-reasoning-type
Replace simulate streaming v5/replace-simulate-streaming
Replace textDelta with text v5/replace-textdelta-with-text
Replace usage token properties v5/replace-usage-token-properties
Restructure file stream parts v5/restructure-file-stream-parts
Restructure source stream parts v5/restructure-source-stream-parts
RSC package v5/rsc-package

v5 베타 버전 간 변경 사항 (Changes Between v5 Beta Versions)

이 섹션은 AI SDK 5.0의 서로 다른 베타 버전 간의 breaking change를 문서화합니다. 이전 v5 베타 버전에서 이후 버전으로 업그레이드한다면 코드에 영향을 줄 수 있는 변경이 있는지 이 섹션을 확인하세요.

fullStream 타입 이름 변경: text/reasoning → text-delta/reasoning-delta

fullStream의 chunk 타입이 UI 스트림 및 언어 모델 스트림과의 일관성을 위해 이름이 변경됐어요.

for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'text-delta': {
      process.stdout.write(chunk.text);
      break;
    }
    case 'reasoning': {
      console.log('Reasoning:', chunk.text);
      break;
    }
  }
}
for await (const chunk of result.fullStream) {
  switch (chunk.type) {
    case 'text-delta': {
      process.stdout.write(chunk.text);
      break;
    }
    case 'reasoning-delta': {
      console.log('Reasoning:', chunk.text);
      break;
    }
  }
}

더 알아보기 (Learn more)

Full Sitemap