Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: vercel/ai
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: ai@3.0.15
Choose a base ref
...
head repository: vercel/ai
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: ai@3.0.16
Choose a head ref
  • 6 commits
  • 31 files changed
  • 5 contributors

Commits on Mar 27, 2024

  1. feat(ai/rsc): Add useStreamableValue again (#1233)

    shuding authored Mar 27, 2024
    Copy the full SHA
    a54ea77 View commit details

Commits on Mar 28, 2024

  1. Refactor prompt validation. (#1247)

    lgrammel authored Mar 28, 2024
    Copy the full SHA
    fafc8d5 View commit details
  2. Add basic ai/core docs (#1240)

    lgrammel authored Mar 28, 2024
    Copy the full SHA
    c3ecd86 View commit details
  3. Fix small typo in the docs (#1251)

    TasseDeCafe authored Mar 28, 2024
    Copy the full SHA
    57e5d34 View commit details
  4. ai/core docs improvements (#1249)

    lgrammel authored Mar 28, 2024
    Copy the full SHA
    6df5213 View commit details

Commits on Mar 29, 2024

  1. Version Packages (#1242)

    Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
    github-actions[bot] and github-actions[bot] authored Mar 29, 2024
    Copy the full SHA
    9610993 View commit details
Showing with 1,510 additions and 158 deletions.
  1. +1 −0 docs/pages/docs/_meta.json
  2. +18 −0 docs/pages/docs/ai-core/_meta.json
  3. +94 −0 docs/pages/docs/ai-core/custom-provider.mdx
  4. +101 −0 docs/pages/docs/ai-core/generate-object.mdx
  5. +129 −0 docs/pages/docs/ai-core/generate-text.mdx
  6. +80 −0 docs/pages/docs/ai-core/index.mdx
  7. +48 −0 docs/pages/docs/ai-core/mistral.mdx
  8. +78 −0 docs/pages/docs/ai-core/openai.mdx
  9. +162 −0 docs/pages/docs/ai-core/prompt.mdx
  10. +47 −0 docs/pages/docs/ai-core/settings.mdx
  11. +106 −0 docs/pages/docs/ai-core/stream-object.mdx
  12. +277 −0 docs/pages/docs/ai-core/stream-text.mdx
  13. +62 −0 docs/pages/docs/ai-core/tools.mdx
  14. +11 −12 examples/ai-core/src/stream-object/openai.ts
  15. +6 −0 packages/core/CHANGELOG.md
  16. +31 −19 packages/core/core/generate-object/generate-object.ts
  17. +27 −15 packages/core/core/generate-object/stream-object.ts
  18. +8 −11 packages/core/core/generate-text/generate-text.ts
  19. +4 −7 packages/core/core/generate-text/stream-text.ts
  20. +67 −66 packages/core/core/prompt/convert-to-language-model-prompt.ts
  21. +0 −16 packages/core/core/prompt/get-input-format.ts
  22. +47 −0 packages/core/core/prompt/get-validated-prompt.ts
  23. +0 −1 packages/core/mistral/mistral-facade.ts
  24. +1 −1 packages/core/package.json
  25. +1 −0 packages/core/rsc/index.ts
  26. +1 −0 packages/core/rsc/rsc-client.ts
  27. +1 −0 packages/core/rsc/rsc-shared.mts
  28. +1 −1 packages/core/rsc/shared-client/index.ts
  29. +99 −7 packages/core/rsc/shared-client/streamable.tsx
  30. +1 −1 packages/core/rsc/streamable.tsx
  31. +1 −1 packages/core/rsc/types.ts
1 change: 1 addition & 0 deletions docs/pages/docs/_meta.json
Original file line number Diff line number Diff line change
@@ -10,5 +10,6 @@
"concepts": "Concepts",
"guides": "Guides",
"api-reference": "API Reference",
"ai-core": "AI Core (Experimental)",
"acknowledgements": "Acknowledgements"
}
18 changes: 18 additions & 0 deletions docs/pages/docs/ai-core/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"index": {
"title": "Overview",
"theme": {
"breadcrumb": false
}
},
"prompt": "Prompts",
"settings": "Settings",
"tools": "Tools and Tool Calling",
"openai": "OpenAI Provider",
"mistral": "Mistral Provider",
"custom-provider": "Custom Providers",
"generate-text": "generateText API",
"stream-text": "streamText API",
"generate-object": "generateObject API",
"stream-object": "streamObject API"
}
94 changes: 94 additions & 0 deletions docs/pages/docs/ai-core/custom-provider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Custom Providers
---

import { Callout } from 'nextra-theme-docs';

# Custom Providers

<Callout>
The AI SDK Language Model Specification is experimental. It may change in the
future without a major version bump.
</Callout>

The AI SDK provides a language model specification.
You can write your own providers that adhere to the AI SDK language model specification and they will be compatible with the AI Core functions.

You can find the Language Model Specification in the [AI SDK repository](https://github.com/vercel/ai/tree/main/packages/core/spec/language-model/v1).
It can be imported from `ai/spec`.

We provide an [OpenAI reference implementation](https://github.com/vercel/ai/tree/main/packages/core/openai)
and a [Mistral reference implementation](https://github.com/vercel/ai/tree/main/packages/core/mistral).

## Provider Facade

A custom provider should follow the pattern of using a provider facade with factory methods for the specific providers.
An instance of the custom provider class with default settings can be exported for convenience.

```ts filename="custom-provider-facade.ts"
import { generateId, loadApiKey } from 'ai/spec';
import { CustomChatLanguageModel } from './custom-chat-language-model';
import { CustomChatModelId, CustomChatSettings } from './mistral-chat-settings';

/**
* Custom provider facade.
*/
export class CustomProvider {
readonly baseUrl?: string;
readonly apiKey?: string;

constructor(
options: {
baseUrl?: string;
apiKey?: string;
} = {},
) {
this.baseUrl = options.baseUrl;
this.apiKey = options.apiKey;
}

private get baseConfig() {
return {
baseUrl: this.baseUrl ?? 'https://custom.ai/v1',
headers: () => ({
Authorization: `Bearer ${loadApiKey({
apiKey: this.apiKey,
environmentVariableName: 'CUSTOM_API_KEY',
description: 'Custom Provider',
})}`,
}),
};
}

chat(modelId: CustomChatModelId, settings: CustomChatSettings = {}) {
return new CustomChatLanguageModel(modelId, settings, {
provider: 'custom.chat',
...this.baseConfig,
});
}
}

/**
* Default custom provider instance.
*/
export const customprovider = new CustomProvider();
```

## Language Model Implementation

Please refer to the Language Model Specification in the [AI SDK repository](https://github.com/vercel/ai/tree/main/packages/core/spec/language-model/v1).

We provide an [OpenAI reference implementation](https://github.com/vercel/ai/tree/main/packages/core/openai)
and a [Mistral reference implementation](https://github.com/vercel/ai/tree/main/packages/core/mistral).

### Errors

The AI SDK provides [standardized errors](https://github.com/vercel/ai/tree/main/packages/core/spec/errors) that should be used by providers where possible.
This will make it easy for user to debug them.

### Retries, timeouts, and abort signals

The AI SDK will handle retries, timeouts, and aborting requests in a unified way.
The model classes should not implement retries or timeouts themselves.
Instead, they should use the abortSignal parameter to determine when the call should be aborted, and they should throw `ApiCallErrors` (or similar)
with a correct `isRetryable` flag when errors such as network errors occur.
101 changes: 101 additions & 0 deletions docs/pages/docs/ai-core/generate-object.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: generateObject API
---

import { Callout } from 'nextra-theme-docs';

# experimental_generateObject

Generate a typed, structured object for a given prompt and [Zod](https://zod.dev/) schema using a language model.

You can use `generateObject` to force the language model to return structured data, e.g. for information extraction,
synthetic data generation, or classification tasks.

```ts
import { experimental_generateObject } from 'ai';

const { object } = await experimental_generateObject({
model,
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
```

<Callout type="info">
`experimental_generateObject` does not stream the output. If you want to
stream the output, use
[`experimental_streamObject`](/docs/ai-core/stream-object).
</Callout>

## Parameters

The parameters are passed into `experimental_streamText` as a single options object.

- **model** - The language model to use.
- **schema** - A [Zod](https://zod.dev/) schema that describes the expected output structure.
- **mode** - The mode to use for object generation. Not all models support all modes. Defaults to 'auto'.
- **system** - A system message that will be apart of the prompt.
- **prompt** - A simple text prompt. You can either use `prompt` or `messages` but not both.
- **messages** - A list of messages. You can either use `prompt` or `messages` but not both.
- **maxTokens** - Maximum number of tokens to generate.
- **temperature** - Temperature setting.
This is a number between 0 (almost no randomness) and 1 (very random).
It is recommended to set either `temperature` or `topP`, but not both.
- **topP** - Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either `temperature` or `topP`, but not both.
- **presencePenalty** - Presence penalty setting.
It affects the likelihood of the model to repeat information that is already in the prompt.
The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **frequencyPenalty** - Frequency penalty setting.
It affects the likelihood of the model to repeatedly use the same words or phrases.
The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **seed** - The seed (integer) to use for random sampling.
If set and supported by the model, calls will generate deterministic results.
- **maxRetries** - Maximum number of retries. Set to 0 to disable retries. Default: 2.
- **abortSignal** - An optional abort signal that can be used to cancel the call.

## Return Type

`generateObject` returns a result object with several properties:

- **object**: `T` - The generated object. Typed based on the schema parameter. The object is validated using Zod.
- **finishReason**: `stop` | `length` | `content-filter` | `tool-calls` | `error` | `other` - The reason why the generation finished.
- **usage**: `TokenUsage` - The token usage of the generated text. The object contains `promptTokens: number`, `completionTokens: number`, and `totalTokens: number`.
- **warnings**: `Array<Warning> | undefined` - Warnings from the model provider (e.g., unsupported settings).

## Examples

### Basic call

```ts
const { object } = await experimental_generateObject({
model,
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
```
129 changes: 129 additions & 0 deletions docs/pages/docs/ai-core/generate-text.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
title: generateText API
---

import { Callout } from 'nextra-theme-docs';

# experimental_generateText

Generate a text and call tools for a given prompt using a language model.

`experimental_generateText` is ideal for non-interactive use cases such as automation tasks where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools.

```ts
import { experimental_generateText } from 'ai';

const { text } = await experimental_generateText({
model,
prompt: 'Invent a new holiday and describe its traditions.',
});
```

<Callout type="info">
`experimental_generateText` does not stream the output. If you want to stream
the output, use [`experimental_streamText`](/docs/ai-core/stream-text).
</Callout>

## Parameters

The parameters are passed into `experimental_streamText` as a single options object.

- **model** - The language model to use.
- **tools** - The tools that the model can call. The model needs to support calling tools.
- **system** - A system message that will be apart of the prompt.
- **prompt** - A simple text prompt. You can either use `prompt` or `messages` but not both.
- **messages** - A list of messages. You can either use `prompt` or `messages` but not both.
- **maxTokens** - Maximum number of tokens to generate.
- **temperature** - Temperature setting.
This is a number between 0 (almost no randomness) and 1 (very random).
It is recommended to set either `temperature` or `topP`, but not both.
- **topP** - Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either `temperature` or `topP`, but not both.
- **presencePenalty** - Presence penalty setting.
It affects the likelihood of the model to repeat information that is already in the prompt.
The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **frequencyPenalty** - Frequency penalty setting.
It affects the likelihood of the model to repeatedly use the same words or phrases.
The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **seed** - The seed (integer) to use for random sampling.
If set and supported by the model, calls will generate deterministic results.
- **maxRetries** - Maximum number of retries. Set to 0 to disable retries. Default: 2.
- **abortSignal** - An optional abort signal that can be used to cancel the call.

## Return Type

`generateText` returns a result object with several properties:

- **text**: `string` - The generated text.
- **toolCalls**: `Array<ToolCall>` - The tool calls that were made during the generation. Each tool call has a `toolCallId`, a `toolName`, and a typed `args` object.
- **toolResults**: `Array<ToolResult>` - The results of the tool calls. Each tool result has a `toolCallId`, a `toolName`, a typed `args` object, and a typed `result` object.
- **finishReason**: `stop` | `length` | `content-filter` | `tool-calls` | `error` | `other` - The reason why the generation finished.
- **usage**: `TokenUsage` - The token usage of the generated text. The object contains `promptTokens: number`, `completionTokens: number`, and `totalTokens: number`.
- **warnings**: `Array<Warning> | undefined` - Warnings from the model provider (e.g., unsupported settings).

## Examples

### Basic call

```ts
const { text, usage, finishReason } = await experimental_generateText({
model,
prompt: 'Invent a new holiday and describe its traditions.',
});
```

### Tool Usage

```ts
const result = await experimental_generateText({
model: openai.chat('gpt-3.5-turbo'),
maxTokens: 512,
tools: {
weather: {
description: 'Get the weather in a location',
parameters: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }: { location: string }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
},
cityAttractions: {
parameters: z.object({ city: z.string() }),
},
},
prompt:
'What is the weather in San Francisco and what attractions should I visit?',
});

// typed tool calls:
for (const toolCall of result.toolCalls) {
switch (toolCall.toolName) {
case 'cityAttractions': {
toolCall.args.city; // string
break;
}

case 'weather': {
toolCall.args.location; // string
break;
}
}
}

// typed tool results for tools with execute method:
for (const toolResult of result.toolResults) {
switch (toolResult.toolName) {
case 'weather': {
toolResult.args.location; // string
toolResult.result.location; // string
toolResult.result.temperature; // number
break;
}
}
}
```
80 changes: 80 additions & 0 deletions docs/pages/docs/ai-core/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: AI Core (experimental)
---

import { Callout } from 'nextra-theme-docs';

# AI Core (experimental)

<Callout>
AI Core is an experimental API. The API is not yet stable and may change in
the future without a major version bump.
</Callout>

The Vercel AI SDK offers a unified way of calling large language models (LLMs) that can be used with any [AI Core-compatible provider](/docs/ai-core#language-model-interface).
It provides the following AI functions:

- `generateText` [ [API](/docs/ai-core/generate-text) ] - Generate text and [call tools](/docs/ai-core/tools).
This function is ideal for non-interactive use cases such as automation tasks where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools.
- `streamText` [ [API](/docs/ai-core/stream-text) ] - Stream text and call tools.
You can use the `streamText` function for interactive use cases such as chat bots (with and without tool usage), and text and code diff streaming in UIs. You can also generate UI components with tools (see [Generative UI](/docs/concepts/ai-rsc)).
- `generateObject` [ [API](/docs/ai-core/generate-object) ] - Generate a typed, structured object that matches a [Zod](https://zod.dev/) schema.
You can use this function to force the language model to return structured data, e.g. for information extraction, synthetic data generation, or classification tasks.
- `streamObject` [ [API](/docs/ai-core/stream-object) ] - Stream a structured object that matches a Zod schema.
You can use this function to stream generated UIs in combination with React Server Components (see [Generative UI](/docs/concepts/ai-rsc)).

The AI functions share the same [prompt structure](/docs/ai-core/prompt) and the same [common settings](/docs/ai-core/settings).
The model is created using a language model provider, e.g. the [OpenAI provider](/docs/ai-core/openai) .
Here is a simple example for `generateText`:

```ts
import { experimental_generateText } from 'ai';
import { openai } from 'ai/openai';

const { text } = await experimental_generateText({
model: openai.chat('gpt-3.5-turbo'),
prompt: 'Invent a new holiday and describe its traditions.',
});
```

## Schema Specification and Validation with Zod

Tool usage and structured object generation require the specification of schemas.
The AI SDK uses [Zod](https://zod.dev/), the most popular JavaScript schema validation library, for schema specification and validation.

You can install Zod with

```sh
npm install zod
```

You can then easily specify schemas, for example:

```ts
const recipeSchema = z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
});
```

Such schemas can be used to define parameters for tool calls and to generated structured objects with `generateObject` and `streamObject`.

## Language Model Interface

Providers need to provide an implementation of the language model interface to be compatible with the AI SDK.
The AI SDK contains the following providers:

- [OpenAI Provider](/docs/ai-core/openai) (`ai/openai`)
- [Mistral Provider](/docs/ai-core/mistral) (`ai/mistral`)

The AI SDK also provides a [language model specification](https://github.com/vercel/ai/tree/main/packages/core/spec/language-model/v1) that you can use to implement [custom providers](/docs/ai-core/custom-provider).

![AI SDK Diagram](/images/ai-sdk-diagram.png)
48 changes: 48 additions & 0 deletions docs/pages/docs/ai-core/mistral.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: Mistral Provider
---

import { Callout } from 'nextra-theme-docs';

# Mistral Provider

The Mistral provider contains language model support for the Mistral chat API.
It creates language model objects that can be used with the `generateText`, `streamText`, `generateObject`, and `streamObject` AI functions.

## Provider Instance

You can import `Mistral` from `ai/mistral` and initialize a provider instance with various settings:

```ts
import { Mistral } from 'ai/mistral';

const mistral = new Mistral({
baseUrl: '', // optional base URL for proxies etc.
apiKey: '', // optional API key, default to env property MISTRAL_API_KEY
});
```

The AI SDK also provides a shorthand `mistral` import with a Mistral provider instance that uses defaults:

```ts
import { mistral } from 'ai/mistral';
```

## Chat Models

You can create models that call the [Mistral chat API](https://docs.mistral.ai/api/#operation/createChatCompletion) using the `.chat()` factory method.
The first argument is the model id, e.g. `mistral-large-latest`.
Some Mistral chat models support tool calls.

```ts
const model = mistral.chat('mistral-large-latest');
```

Mistral chat models also support additional model settings that are not part of the [standard call settings](/docs/ai-core/settings).
You can pass them as an options argument:

```ts
const model = mistral.chat('mistral-large-latest', {
safePrompt: true, // optional safety prompt injection
});
```
78 changes: 78 additions & 0 deletions docs/pages/docs/ai-core/openai.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: OpenAI Provider
---

import { Callout } from 'nextra-theme-docs';

# OpenAI Provider

The OpenAI provider contains language model support for the OpenAI chat and completion APIs.
It creates language model objects that can be used with the `generateText`, `streamText`, `generateObject`, and `streamObject` AI functions.

## Provider Instance

You can import `OpenAI` from `ai/openai` and initialize a provider instance with various settings:

```ts
import { OpenAI } from 'ai/openai'

const openai = new OpenAI({
baseUrl: '', // optional base URL for proxies etc.
apiKey: '' // optional API key, default to env property OPENAI_API_KEY
organization: '' // optional organization
})
```

The AI SDK also provides a shorthand `openai` import with an OpenAI provider instance that uses defaults:

```ts
import { openai } from 'ai/openai';
```

## Chat Models

You can create models that call the [OpenAI chat API](https://platform.openai.com/docs/api-reference/chat) using the `.chat()` factory method.
The first argument is the model id, e.g. `gpt-4`.
The OpenAI chat models support tool calls and some have multi-modal capabilities.

```ts
const model = openai.chat('gpt-3.5-turbo');
```

OpenAI chat models support also some model specific settings that are not part of the [standard call settings](/docs/ai-core/settings).
You can pass them as an options argument:

```ts
const model = openai.chat('gpt-3.5-turbo', {
logitBias: {
// optional likelihood for specific tokens
'50256': -100,
},
user: 'test-user', // optional unique user identifier
});
```

## Completion Models

You can create models that call the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) using the `.completion()` factory method.
The first argument is the model id.
Currently only `gpt-3.5-turbo-instruct` is supported.

```ts
const model = openai.completion('gpt-3.5-turbo-instruct');
```

OpenAI completion models support also some model specific settings that are not part of the [standard call settings](/docs/ai-core/settings).
You can pass them as an options argument:

```ts
const model = openai.chat('gpt-3.5-turbo', {
echo: true, // optional, echo the prompt in addition to the completion
logitBias: {
// optional likelihood for specific tokens
'50256': -100,
},
suffix: 'some text', // optional suffix that comes after a completion of inserted text
user: 'test-user', // optional unique user identifier
});
```
162 changes: 162 additions & 0 deletions docs/pages/docs/ai-core/prompt.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: Prompts
---

import { Callout } from 'nextra-theme-docs';

# Prompts

All AI functions (`generateText`, `streamText`, `generateObject`, `streamObject`) support the same prompt types.
You can use either a text prompt or a more elaborate message prompt.
Both prompt types support system messages.

The prompts get translated by the model that you use to a value that works for the provider API,
e.g. a text prompt could become a provider user message to match a provider chat API.

## Text Prompts

Text prompts are just that: a simple string.
They are ideal for simpler generation use cases,
e.g. when you want to repeatedly generate content for variants of the same prompt text.

You can set text prompts using the `prompt` property.
You can structure the text in any way and inject variables, e.g. using a template literal.

### Basic Example

```ts {3}
const result = await experimental_generateText({
model,
prompt: 'Invent a new holiday and describe its traditions.',
});
```

### Template Literal Example

```ts {3-5}
const result = await experimental_generateText({
model,
prompt:
`I am planning a trip to ${destination} for ${lengthOfStay} days. ` +
`Please suggest the best tourist activities for me to do.`,
});
```

## Message Prompts

Message prompts are an array of user, assistant, and tool messages.
They are great for chat interfaces and more complex, multi-modal prompts.

Each message has a role and a content. The content can be either a text
(for user and assistant messages), or an array of parts for that message type.

<Callout type="info">
Not all language models support all message and content types. For example,
some models might not be capable of handling multi-modal inputs or tool
messages.
</Callout>

### Basic Example

```ts {3-7}
const result = await experimental_generateText({
model,
messages: [
{ role: 'user', content: 'Hi!' },
{ role: 'assistant', content: 'Hello, how can I help?' },
{ role: 'user', content: 'Where can I buy the best Currywurst in Berlin?' },
],
});
```

### Multi-modal Messages

The `image` can be a base64-encoded image (`string`), an `ArrayBuffer`, a `Uint8Array`,
a `Buffer`, or a `URL` object. It is possible to mix text and multiple images.

```ts {3-11}
const result = await experimental_generateText({
model,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe the image in detail.' },
{ type: 'image', image: fs.readFileSync('./data/comic-cat.png') },
],
},
],
});
```

### Tool Messages

For models that support tool calls, assistant messages can contain tool call parts, and tool messages can contain tool result parts.
A single assistant message can call multiple tools, and a single tool message can contain multiple tool results.

```ts {3-43}
const result = await experimental_generateText({
model,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'How many calories are in this block of cheese?',
},
{ type: 'image', image: fs.readFileSync('./data/roquefort.jpg') },
],
},
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: '12345',
toolName: 'get-nutrition-data',
args: { cheese: 'Roquefort' },
},
// there could be more tool calls here (parallel calling)
],
},
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: '12345', // needs to match the tool call id
toolName: 'get-nutrition-data',
result: {
name: 'Cheese, roquefort',
calories: 369,
fat: 31,
protein: 22,
},
},
// there could be more tool results here (parallel calling)
],
},
],
});
```

## System Messages

You can set system prompts using the separate `system` property.
System messages work with both the `prompt` and the `messages` properties.

### Example

```ts {3-6}
const result = await experimental_generateText({
model,
system:
`You help planning travel itineraries. ` +
`Respond to the users' request with a list ` +
`of the best stops to make in their destination.`,
prompt:
`I am planning a trip to ${destination} for ${lengthOfStay} days. ` +
`Please suggest the best tourist activities for me to do.`,
});
```
47 changes: 47 additions & 0 deletions docs/pages/docs/ai-core/settings.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Settings
---

import { Callout } from 'nextra-theme-docs';

# Settings

All AI functions (`generateText`, `streamText`, `generateObject`, `streamObject`) support the following common settings in addition to the model and the [prompt](/docs/ai-core/prompt):

- **maxTokens** - Maximum number of tokens to generate.
- **temperature** - Temperature setting.
This is a number between 0 (almost no randomness) and 1 (very random).
It is recommended to set either `temperature` or `topP`, but not both.
- **topP** - Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either `temperature` or `topP`, but not both.
- **presencePenalty** - Presence penalty setting.
It affects the likelihood of the model to repeat information that is already in the prompt.
The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **frequencyPenalty** - Frequency penalty setting.
It affects the likelihood of the model to repeatedly use the same words or phrases.
The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **seed** - The seed (integer) to use for random sampling.
If set and supported by the model, calls will generate deterministic results.
- **maxRetries** - Maximum number of retries. Set to 0 to disable retries. Default: 2.
- **abortSignal** - An optional abort signal that can be used to cancel the call.

<Callout type="info">
Some providers do not support all common settings. If you use a setting with a
provider that does not support it, a warning will be included in the AI
function result object.
</Callout>

## Example

```ts {3-5}
const result = await experimental_generateText({
model,
maxTokens: 512,
temperature: 0.3,
maxRetries: 5,
prompt: 'Invent a new holiday and describe its traditions.',
});
```
106 changes: 106 additions & 0 deletions docs/pages/docs/ai-core/stream-object.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: streamObject API
---

import { Callout } from 'nextra-theme-docs';

# experimental_streamObject

Streams a typed, structured object for a given prompt and [Zod](https://zod.dev/) schema using a language model.

You can use `streamObject` to stream generated UIs in combination with React Server Components (see [Generative UI](/docs/concepts/ai-rsc)).

```ts
import { experimental_generateObject } from 'ai';

const { partialObjectStream } = await experimental_streamObject({
model,
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
```

<Callout type="info">
`experimental_streamObject` streams the output. If you do not want to stream
the output, use
[`experimental_generateObject`](/docs/ai-core/generate-object).
</Callout>

## Parameters

The parameters are passed into `experimental_streamText` as a single options object.

- **model** - The language model to use.
- **schema** - A [Zod](https://zod.dev/) schema that describes the expected output structure.
- **mode** - The mode to use for object generation. Not all models support all modes. Defaults to 'auto'.
- **system** - A system message that will be apart of the prompt.
- **prompt** - A simple text prompt. You can either use `prompt` or `messages` but not both.
- **messages** - A list of messages. You can either use `prompt` or `messages` but not both.
- **maxTokens** - Maximum number of tokens to generate.
- **temperature** - Temperature setting.
This is a number between 0 (almost no randomness) and 1 (very random).
It is recommended to set either `temperature` or `topP`, but not both.
- **topP** - Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either `temperature` or `topP`, but not both.
- **presencePenalty** - Presence penalty setting.
It affects the likelihood of the model to repeat information that is already in the prompt.
The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **frequencyPenalty** - Frequency penalty setting.
It affects the likelihood of the model to repeatedly use the same words or phrases.
The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **seed** - The seed (integer) to use for random sampling.
If set and supported by the model, calls will generate deterministic results.
- **maxRetries** - Maximum number of retries. Set to 0 to disable retries. Default: 2.
- **abortSignal** - An optional abort signal that can be used to cancel the call.

## Return Type

`streamObject` returns a result object with several properties:

- **partialObjectStream**: `AsyncIterable<DeepPartial<T>> & ReadableStream<DeepPartial<T>>` - A stream of partial objects.

<Callout type="info">
The `partialObjectStream` is typed with a deep partial type, but not
validated. If you want to be certain that the actual content matches your
schema, you need to implement your own validation for partial results, e.g.
using Zod.
</Callout>

- **warnings**: `Array<Warning> | undefined` - Warnings from the model provider (e.g., unsupported settings).

## Examples

### Basic call

```ts
const { partialObjectStream } = await experimental_streamObject({
model,
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
}),
),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a lasagna recipe.',
});
```
277 changes: 277 additions & 0 deletions docs/pages/docs/ai-core/stream-text.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
---
title: streamText API
---

import { Callout } from 'nextra-theme-docs';

# experimental_streamText

Streams text and call tools for a given prompt using a language model.

You can use the `streamText` function for interactive use cases such as chat bots (with and without tool usage), and text and code diff streaming in UIs. You can also generate UI components with tools (see [Generative UI](/docs/concepts/ai-rsc)).

```ts
import { experimental_streamText } from 'ai';

const { textStream } = await experimental_streamText({
model,
prompt: 'Invent a new holiday and describe its traditions.',
});
```

<Callout type="info">
`experimental_streamText` streams the output. If you do not want to stream the
output, use [`experimental_generateText`](/docs/ai-core/generate-text).
</Callout>

## Parameters

The parameters are passed into `experimental_streamText` as a single options object.

- **model** - The language model to use.
- **tools** - The tools that the model can call. The model needs to support calling tools.
- **system** - A system message that will be apart of the prompt.
- **prompt** - A simple text prompt. You can either use `prompt` or `messages` but not both.
- **messages** - A list of messages. You can either use `prompt` or `messages` but not both.
- **maxTokens** - Maximum number of tokens to generate.
- **temperature** - Temperature setting.
This is a number between 0 (almost no randomness) and 1 (very random).
It is recommended to set either `temperature` or `topP`, but not both.
- **topP** - Nucleus sampling. This is a number between 0 and 1.
E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered.
It is recommended to set either `temperature` or `topP`, but not both.
- **presencePenalty** - Presence penalty setting.
It affects the likelihood of the model to repeat information that is already in the prompt.
The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **frequencyPenalty** - Frequency penalty setting.
It affects the likelihood of the model to repeatedly use the same words or phrases.
The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition).
0 means no penalty.
- **seed** - The seed (integer) to use for random sampling.
If set and supported by the model, calls will generate deterministic results.
- **maxRetries** - Maximum number of retries. Set to 0 to disable retries. Default: 2.
- **abortSignal** - An optional abort signal that can be used to cancel the call.

## Return Type

`streamText` returns a result object with several properties and methods:

- **textStream**: `AsyncIterable<string> & ReadableStream<string>` - A stream of text deltas.
- **fullStream**: `AsyncIterable<TextStreamPart> & ReadableStream<TextStreamPart>` - A stream of text stream parts:
- **text delta** stream part with `type = 'text-delta'` and a `textDelta` property.
- **tool call** stream part with `type = 'tool-call'` and `toolCallId`, `toolName`, and `args` properties.
- **tool result** stream part with `type = 'tool-result'` and `toolCallId`, `toolName`, `args`, and `result` properties.
- **finish** stream part with `type = 'finish'` and `finishReason` and `usage` properties. Sent as the last stream part.
- **error** stream part with `type = 'error'` and an `error` property.
- **toAIStream(callbacks)**: convert the stream to an AI stream that is compatible with [`StreamingTextResponse`](/docs/api-reference/streaming-text-response).
- **warnings**: `Array<Warning> | undefined` - Warnings from the model provider (e.g., unsupported settings).

<Callout type="info">
You can only process one of the streams. Once it is processing, the other
stream properties will throw errors.
</Callout>

## Examples

### Basic call

```ts
const { textStream } = await experimental_streamText({
model,
prompt: 'Invent a new holiday and describe its traditions.',
});
```

### API Route for useCompletion

This example shows an [App router](https://nextjs.org/docs/app) API route that is used
by the [`useCompletion`](/docs/api-reference/use-completion) hook.

```ts
import { StreamingTextResponse, experimental_streamText } from 'ai';
import { openai } from 'ai/openai';

export const runtime = 'edge';

export async function POST(req: Request) {
// Extract the `prompt` text from the body of the request
const { prompt }: { prompt: string } = await req.json();

const result = await experimental_streamText({
model: openai.chat('gpt-4'),
prompt,
});

return new StreamingTextResponse(result.toAIStream());
}
```

### API Route for useChat

This example shows an [App router](https://nextjs.org/docs/app) API route that is used
by the [`useChat`](/docs/api-reference/use-chat) hook.

```ts
import { StreamingTextResponse, experimental_streamText } from 'ai';
import { openai } from 'ai/openai';

export const runtime = 'edge';

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

const result = await experimental_streamText({
model: openai.chat('gpt-4'),
system: 'You are a helpful assistant.',
messages,
});

return new StreamingTextResponse(result.toAIStream());
}
```

### Terminal chatbot

```ts
import { ExperimentalMessage, experimental_streamText } from 'ai';
import { openai } from 'ai/openai';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const messages: ExperimentalMessage[] = [];

async function main() {
while (true) {
const userInput = await terminal.question('You: ');

messages.push({ role: 'user', content: userInput });

const result = await experimental_streamText({
model: openai.chat('gpt-3.5-turbo'),
system: `You are a helpful, respectful and honest assistant.`,
messages,
});

let fullResponse = '';
process.stdout.write('\nAssistant: ');
for await (const delta of result.textStream) {
fullResponse += delta;
process.stdout.write(delta);
}
process.stdout.write('\n\n');

messages.push({ role: 'assistant', content: fullResponse });
}
}

main().catch(console.error);
```

### Terminal chatbot with tools

```ts
import {
ExperimentalMessage,
ToolCallPart,
ToolResultPart,
experimental_streamText,
} from 'ai';
import { openai } from 'ai/openai';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const messages: ExperimentalMessage[] = [];

async function main() {
let toolResponseAvailable = false;

while (true) {
if (!toolResponseAvailable) {
const userInput = await terminal.question('You: ');
messages.push({ role: 'user', content: userInput });
}

const result = await experimental_streamText({
model: openai.chat('gpt-3.5-turbo'),
tools: {
weatherTool: {
description: 'Get the weather in a location',
parameters: z.object({
location: z
.string()
.describe('The location to get the weather for'),
}),
execute: async ({ location }: { location: string }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
},
},
system: `You are a helpful, respectful and honest assistant.`,
messages,
});

toolResponseAvailable = false;
let fullResponse = '';
const toolCalls: ToolCallPart[] = [];
const toolResponses: ToolResultPart[] = [];

for await (const delta of result.fullStream) {
switch (delta.type) {
case 'text-delta': {
if (fullResponse.length === 0) {
process.stdout.write('\nAssistant: ');
}

fullResponse += delta.textDelta;
process.stdout.write(delta.textDelta);
break;
}

case 'tool-call': {
toolCalls.push(delta);

process.stdout.write(
`\nTool call: '${delta.toolName}' ${JSON.stringify(delta.args)}`,
);
break;
}

case 'tool-result': {
toolResponses.push(delta);

process.stdout.write(
`\nTool response: '${delta.toolName}' ${JSON.stringify(
delta.result,
)}`,
);
break;
}
}
}
process.stdout.write('\n\n');

messages.push({
role: 'assistant',
content: [{ type: 'text', text: fullResponse }, ...toolCalls],
});

if (toolResponses.length > 0) {
messages.push({ role: 'tool', content: toolResponses });
}

toolResponseAvailable = toolCalls.length > 0;
}
}

main().catch(console.error);
```
62 changes: 62 additions & 0 deletions docs/pages/docs/ai-core/tools.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Tools and Tool Calling
---

import { Callout } from 'nextra-theme-docs';
import { Steps } from 'nextra/components';

# Tools and Tool Calling

The `generateObject` and `streamObject` AI functions support tool calls for models with such a capability.
The tool call and tool results objects are typed.
Tool calling works as follows:

<Steps>
### Add tools to `generateText` or `streamText`

You define the tools and pass them into `generateText` or `streamText` using the `tools` parameter.

AI Core tools are objects that implement the `ExperimentalTool<PARAMETERS, RESULT>` interface with the following properties:

- **description**: An optional description of the tool. This information is provided to the language model and can influence when the tool is picked.
- **parameters**: A [Zod](https://zod.dev/) schema that defines the parameters. It is converted to a JSON schema that is consumed by the LLM, and also used to validate the LLM tool calls.
- **execute**: An optional async function that is called with the arguments from the tool call and produces a value of type `RESULT`.

The `tools` parameter is an object that has the tool names as keys and the tools as values:

```ts {3-17}
const result = await experimental_generateText({
model: openai.chat('gpt-3.5-turbo'),
tools: {
weather: {
description: 'Get the weather in a location',
parameters: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }: { location: string }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
},
cityAttractions: {
parameters: z.object({ city: z.string() }),
},
},
prompt:
'What is the weather in San Francisco and what attractions should I visit?',
});
```

### The language model generates tool calls

The language model will invoke the tools when it makes sense.
The tool calls are provided as tool call objects.
Each tool call has a `toolCallId`, a `toolName`, and a typed `args` object.

### Tools are executed

Tools which have an `execute` function are run automatically when corresponding tool calls are generated.
The results of the tool executions are provided using tool result objects.
Each tool result object has a `toolCallId`, a `toolName`, a typed `args` object, and a typed `result`.

</Steps>
23 changes: 11 additions & 12 deletions examples/ai-core/src/stream-object/openai.ts
Original file line number Diff line number Diff line change
@@ -8,21 +8,20 @@ dotenv.config();
const openai = new OpenAI();

async function main() {
const schema = z.object({
characters: z.array(
z.object({
name: z.string(),
class: z
.string()
.describe('Character class, e.g. warrior, mage, or thief.'),
description: z.string(),
}),
),
});
const result = await experimental_streamObject({
model: openai.chat('gpt-4-turbo-preview'),
maxTokens: 2000,
schema: schema,
schema: z.object({
characters: z.array(
z.object({
name: z.string(),
class: z
.string()
.describe('Character class, e.g. warrior, mage, or thief.'),
description: z.string(),
}),
),
}),
prompt:
'Generate 3 character descriptions for a fantasy role playing game.',
});
6 changes: 6 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# ai

## 3.0.16

### Patch Changes

- a54ea77: feat(ai/rsc): add `useStreamableValue`

## 3.0.15

### Patch Changes
50 changes: 31 additions & 19 deletions packages/core/core/generate-object/generate-object.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import {
import { TokenUsage, calculateTokenUsage } from '../generate-text/token-usage';
import { CallSettings } from '../prompt/call-settings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { getInputFormat } from '../prompt/get-input-format';
import { getValidatedPrompt } from '../prompt/get-validated-prompt';
import { prepareCallSettings } from '../prompt/prepare-call-settings';
import { Prompt } from '../prompt/prompt';
import { retryWithExponentialBackoff } from '../util/retry-with-exponential-backoff';
@@ -22,7 +22,9 @@ Generate a structured, typed object for a given prompt and schema using a langua
This function does not stream the output. If you want to stream the output, use `experimental_streamObject` instead.
@param model - The language model to use.
@param schema - The schema of the object that the model should generate.
@param mode - The mode to use for object generation. Not all models support all modes. Defaults to 'auto'.
@param system - A system message that will be part of the prompt.
@param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both.
@@ -96,19 +98,21 @@ Default and recommended: 'auto' (best mode for the model).

switch (mode) {
case 'json': {
const generateResult = await retry(() =>
model.doGenerate({
const validatedPrompt = getValidatedPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
});

const generateResult = await retry(() => {
return model.doGenerate({
mode: { type: 'object-json' },
...prepareCallSettings(settings),
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
}),
);
});
});

if (generateResult.text === undefined) {
throw new NoTextGeneratedError();
@@ -123,16 +127,18 @@ Default and recommended: 'auto' (best mode for the model).
}

case 'grammar': {
const validatedPrompt = getValidatedPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
});

const generateResult = await retry(() =>
model.doGenerate({
mode: { type: 'object-grammar', schema: jsonSchema },
...settings,
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
}),
);
@@ -150,6 +156,12 @@ Default and recommended: 'auto' (best mode for the model).
}

case 'tool': {
const validatedPrompt = getValidatedPrompt({
system,
prompt,
messages,
});

const generateResult = await retry(() =>
model.doGenerate({
mode: {
@@ -162,8 +174,8 @@ Default and recommended: 'auto' (best mode for the model).
},
},
...settings,
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({ system, prompt, messages }),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
}),
);
42 changes: 27 additions & 15 deletions packages/core/core/generate-object/stream-object.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import {
} from '../../spec';
import { CallSettings } from '../prompt/call-settings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { getInputFormat } from '../prompt/get-input-format';
import { getValidatedPrompt } from '../prompt/get-validated-prompt';
import { prepareCallSettings } from '../prompt/prepare-call-settings';
import { Prompt } from '../prompt/prompt';
import {
@@ -27,7 +27,9 @@ Generate a structured, typed object for a given prompt and schema using a langua
This function streams the output. If you do not want to stream the output, use `experimental_generateObject` instead.
@param model - The language model to use.
@param schema - The schema of the object that the model should generate.
@param mode - The mode to use for object generation. Not all models support all modes. Defaults to 'auto'.
@param system - A system message that will be part of the prompt.
@param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both.
@@ -99,15 +101,17 @@ Default and recommended: 'auto' (best mode for the model).

switch (mode) {
case 'json': {
const validatedPrompt = getValidatedPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
});

callOptions = {
mode: { type: 'object-json' },
...prepareCallSettings(settings),
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
};

@@ -128,15 +132,17 @@ Default and recommended: 'auto' (best mode for the model).
}

case 'grammar': {
const validatedPrompt = getValidatedPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
});

callOptions = {
mode: { type: 'object-grammar', schema: jsonSchema },
...settings,
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system: injectJsonSchemaIntoSystem({ system, schema: jsonSchema }),
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
};

@@ -157,6 +163,12 @@ Default and recommended: 'auto' (best mode for the model).
}

case 'tool': {
const validatedPrompt = getValidatedPrompt({
system,
prompt,
messages,
});

callOptions = {
mode: {
type: 'object-tool',
@@ -168,8 +180,8 @@ Default and recommended: 'auto' (best mode for the model).
},
},
...settings,
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({ system, prompt, messages }),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
};

19 changes: 8 additions & 11 deletions packages/core/core/generate-text/generate-text.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import {
} from '../../spec';
import { CallSettings } from '../prompt/call-settings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { getInputFormat } from '../prompt/get-input-format';
import { getValidatedPrompt } from '../prompt/get-validated-prompt';
import { prepareCallSettings } from '../prompt/prepare-call-settings';
import { Prompt } from '../prompt/prompt';
import { ExperimentalTool } from '../tool/tool';
@@ -75,8 +75,9 @@ The tools that the model can call. The model needs to support calling tools.
tools?: TOOLS;
}): Promise<GenerateTextResult<TOOLS>> {
const retry = retryWithExponentialBackoff({ maxRetries });
const modelResponse = await retry(() =>
model.doGenerate({
const validatedPrompt = getValidatedPrompt({ system, prompt, messages });
const modelResponse = await retry(() => {
return model.doGenerate({
mode: {
type: 'regular',
tools:
@@ -90,15 +91,11 @@ The tools that the model can call. The model needs to support calling tools.
})),
},
...prepareCallSettings(settings),
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system,
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
}),
);
});
});

// parse tool calls:
const toolCalls: ToToolCallArray<TOOLS> = [];
11 changes: 4 additions & 7 deletions packages/core/core/generate-text/stream-text.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import {
} from '../../streams';
import { CallSettings } from '../prompt/call-settings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { getInputFormat } from '../prompt/get-input-format';
import { getValidatedPrompt } from '../prompt/get-validated-prompt';
import { prepareCallSettings } from '../prompt/prepare-call-settings';
import { Prompt } from '../prompt/prompt';
import { ExperimentalTool } from '../tool';
@@ -82,6 +82,7 @@ The tools that the model can call. The model needs to support calling tools.
tools?: TOOLS;
}): Promise<StreamTextResult<TOOLS>> {
const retry = retryWithExponentialBackoff({ maxRetries });
const validatedPrompt = getValidatedPrompt({ system, prompt, messages });
const { stream, warnings } = await retry(() =>
model.doStream({
mode: {
@@ -97,12 +98,8 @@ The tools that the model can call. The model needs to support calling tools.
})),
},
...prepareCallSettings(settings),
inputFormat: getInputFormat({ prompt, messages }),
prompt: convertToLanguageModelPrompt({
system,
prompt,
messages,
}),
inputFormat: validatedPrompt.type,
prompt: convertToLanguageModelPrompt(validatedPrompt),
abortSignal,
}),
);
133 changes: 67 additions & 66 deletions packages/core/core/prompt/convert-to-language-model-prompt.ts
Original file line number Diff line number Diff line change
@@ -5,88 +5,89 @@ import {
LanguageModelV1TextPart,
} from '../../spec';
import { convertDataContentToUint8Array } from './data-content';
import { Prompt } from './prompt';

export function convertToLanguageModelPrompt({
system,
prompt,
messages,
}: Prompt): LanguageModelV1Prompt {
if (prompt == null && messages == null) {
throw new Error('prompt or messages must be defined');
}

if (prompt != null && messages != null) {
throw new Error('prompt and messages cannot be defined at the same time');
}
import { ValidatedPrompt } from './get-validated-prompt';

export function convertToLanguageModelPrompt(
prompt: ValidatedPrompt,
): LanguageModelV1Prompt {
const languageModelMessages: LanguageModelV1Prompt = [];

if (system != null) {
languageModelMessages.push({ role: 'system', content: system });
if (prompt.system != null) {
languageModelMessages.push({ role: 'system', content: prompt.system });
}

if (typeof prompt === 'string') {
languageModelMessages.push({
role: 'user',
content: [{ type: 'text', text: prompt }],
});
} else {
messages = messages!; // it's not null because of the check above
switch (prompt.type) {
case 'prompt': {
languageModelMessages.push({
role: 'user',
content: [{ type: 'text', text: prompt.prompt }],
});
break;
}

case 'messages': {
languageModelMessages.push(
...prompt.messages.map((message): LanguageModelV1Message => {
switch (message.role) {
case 'user': {
if (typeof message.content === 'string') {
return {
role: 'user',
content: [{ type: 'text', text: message.content }],
};
}

languageModelMessages.push(
...messages.map((message): LanguageModelV1Message => {
switch (message.role) {
case 'user': {
if (typeof message.content === 'string') {
return {
role: 'user',
content: [{ type: 'text', text: message.content }],
};
}
content: message.content.map(
(
part,
): LanguageModelV1TextPart | LanguageModelV1ImagePart => {
switch (part.type) {
case 'text': {
return part;
}

return {
role: 'user',
content: message.content.map(
(part): LanguageModelV1TextPart | LanguageModelV1ImagePart => {
switch (part.type) {
case 'text': {
return part;
case 'image': {
return {
type: 'image',
image:
part.image instanceof URL
? part.image
: convertDataContentToUint8Array(part.image),
mimeType: part.mimeType,
};
}
}
},
),
};
}

case 'image': {
return {
type: 'image',
image:
part.image instanceof URL
? part.image
: convertDataContentToUint8Array(part.image),
mimeType: part.mimeType,
};
}
}
},
),
};
}
case 'assistant': {
if (typeof message.content === 'string') {
return {
role: 'assistant',
content: [{ type: 'text', text: message.content }],
};
}

case 'assistant': {
if (typeof message.content === 'string') {
return {
role: 'assistant',
content: [{ type: 'text', text: message.content }],
};
return { role: 'assistant', content: message.content };
}

return { role: 'assistant', content: message.content };
case 'tool': {
return message;
}
}
}),
);
break;
}

case 'tool': {
return message;
}
}
}),
);
default: {
const _exhaustiveCheck: never = prompt;
throw new Error(`Unsupported prompt type: ${_exhaustiveCheck}`);
}
}

return languageModelMessages;
16 changes: 0 additions & 16 deletions packages/core/core/prompt/get-input-format.ts

This file was deleted.

47 changes: 47 additions & 0 deletions packages/core/core/prompt/get-validated-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { InvalidPromptError } from '../../spec';
import { ExperimentalMessage } from './message';
import { Prompt } from './prompt';

export type ValidatedPrompt =
| {
type: 'prompt';
prompt: string;
messages: undefined;
system?: string;
}
| {
type: 'messages';
prompt: undefined;
messages: ExperimentalMessage[];
system?: string;
};

export function getValidatedPrompt(prompt: Prompt): ValidatedPrompt {
if (prompt.prompt == null && prompt.messages == null) {
throw new InvalidPromptError({
prompt,
message: 'prompt or messages must be defined',
});
}

if (prompt.prompt != null && prompt.messages != null) {
throw new InvalidPromptError({
prompt,
message: 'prompt and messages cannot be defined at the same time',
});
}

return prompt.prompt != null
? {
type: 'prompt',
prompt: prompt.prompt,
messages: undefined,
system: prompt.system,
}
: {
type: 'messages',
prompt: undefined,
messages: prompt.messages!, // only possible case bc of checks above
system: prompt.system,
};
}
1 change: 0 additions & 1 deletion packages/core/mistral/mistral-facade.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ export class Mistral {
options: {
baseUrl?: string;
apiKey?: string;
organization?: string;
generateId?: () => string;
} = {},
) {
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai",
"version": "3.0.15",
"version": "3.0.16",
"license": "Apache-2.0",
"sideEffects": false,
"main": "./dist/index.js",
1 change: 1 addition & 0 deletions packages/core/rsc/index.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ export type {

export type {
readStreamableValue,
useStreamableValue,
useUIState,
useAIState,
useActions,
1 change: 1 addition & 0 deletions packages/core/rsc/rsc-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
readStreamableValue,
useStreamableValue,
useUIState,
useAIState,
useActions,
1 change: 1 addition & 0 deletions packages/core/rsc/rsc-shared.mts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

export {
readStreamableValue,
useStreamableValue,
useUIState,
useAIState,
useActions,
2 changes: 1 addition & 1 deletion packages/core/rsc/shared-client/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

export { readStreamableValue } from './streamable';
export { readStreamableValue, useStreamableValue } from './streamable';
export {
useUIState,
useAIState,
106 changes: 99 additions & 7 deletions packages/core/rsc/shared-client/streamable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { startTransition, useLayoutEffect, useState } from 'react';
import { STREAMABLE_VALUE_TYPE } from '../constants';
import type { StreamableValue } from '../types';

function hasReadableValueSignature(value: unknown): value is StreamableValue {
return !!(
value &&
typeof value === 'object' &&
'type' in value &&
value.type === STREAMABLE_VALUE_TYPE
);
}

function assertStreamableValue(
value: unknown,
): asserts value is StreamableValue {
if (
!value ||
typeof value !== 'object' ||
!('type' in value) ||
value.type !== STREAMABLE_VALUE_TYPE
) {
if (!hasReadableValueSignature(value)) {
throw new Error(
'Invalid value: this hook only accepts values created via `createStreamableValue` from the server.',
'Invalid value: this hook only accepts values created via `createStreamableValue`.',
);
}
}

function isStreamableValue(value: unknown): value is StreamableValue {
const hasSignature = hasReadableValueSignature(value);

if (!hasSignature && typeof value !== 'undefined') {
throw new Error(
'Invalid value: this hook only accepts values created via `createStreamableValue`.',
);
}

return hasSignature;
}

/**
* `readStreamableValue` takes a streamable value created via the `createStreamableValue().value` API,
* and returns an async iterator.
@@ -122,3 +139,78 @@ export function readStreamableValue<T = unknown>(
},
};
}

/**
* `useStreamableValue` is a React hook that takes a streamable value created via the `createStreamableValue().value` API,
* and returns the current value, error, and pending state.
*
* This is useful for consuming streamable values received from a component's props. For example:
*
* ```js
* function MyComponent({ streamableValue }) {
* const [data, error, pending] = useStreamableValue(streamableValue);
*
* if (pending) return <div>Loading...</div>;
* if (error) return <div>Error: {error.message}</div>;
*
* return <div>Data: {data}</div>;
* }
* ```
*/
export function useStreamableValue<T = unknown, Error = unknown>(
streamableValue?: StreamableValue<T>,
): [data: T | undefined, error: Error | undefined, pending: boolean] {
const [curr, setCurr] = useState<T | undefined>(
isStreamableValue(streamableValue) ? streamableValue.curr : undefined,
);
const [error, setError] = useState<Error | undefined>(
isStreamableValue(streamableValue) ? streamableValue.error : undefined,
);
const [pending, setPending] = useState<boolean>(
isStreamableValue(streamableValue) ? !!streamableValue.next : false,
);

useLayoutEffect(() => {
if (!isStreamableValue(streamableValue)) return;

let cancelled = false;

const iterator = readStreamableValue(streamableValue);
if (streamableValue.next) {
startTransition(() => {
if (cancelled) return;
setPending(true);
});
}

(async () => {
try {
for await (const value of iterator) {
if (cancelled) return;
startTransition(() => {
if (cancelled) return;
setCurr(value);
});
}
} catch (e) {
if (cancelled) return;
startTransition(() => {
if (cancelled) return;
setError(e as Error);
});
} finally {
if (cancelled) return;
startTransition(() => {
if (cancelled) return;
setPending(false);
});
}
})();

return () => {
cancelled = true;
};
}, [streamableValue]);

return [curr, error, pending];
}
2 changes: 1 addition & 1 deletion packages/core/rsc/streamable.tsx
Original file line number Diff line number Diff line change
@@ -215,7 +215,7 @@ export function createStreamableValue<T = any, E = any>(initialValue?: T) {
/**
* The value of the streamable. This can be returned from a Server Action and
* received by the client. To read the streamed values, use the
* `readStreamableValue` API.
* `readStreamableValue` or `useStreamableValue` APIs.
*/
get value() {
return createWrapped(true);
2 changes: 1 addition & 1 deletion packages/core/rsc/types.ts
Original file line number Diff line number Diff line change
@@ -91,7 +91,7 @@ export type StreamablePatch = undefined | [0, string]; // Append string.

/**
* StreamableValue is a value that can be streamed over the network via AI Actions.
* To read the streamed values, use the `readStreamableValue` API.
* To read the streamed values, use the `readStreamableValue` or `useStreamableValue` APIs.
*/
export type StreamableValue<T = any, E = any> = {
/**