Open Responses

Learn how to use the Open Responses provider for the AI SDK.

The Open Responses provider connects AI SDK Core to language model servers that implement an Open Responses-compatible POST endpoint. Open Responses is an open specification based on the OpenAI Responses API.

Use @ai-sdk/open-responses for third-party or self-hosted endpoints that implement this protocol, such as LM Studio. If you call OpenAI directly and need OpenAI-specific provider options or built-in tools, use the @ai-sdk/openai provider instead.

Setup

The Open Responses provider is available in the @ai-sdk/open-responses module. You can install it with

$ pnpm add @ai-sdk/open-responses

Provider Instance

Create an Open Responses provider instance using createOpenResponses:

import { createOpenResponses } from '@ai-sdk/open-responses';

const openResponses = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

The name and url options are required:

  • name string

    Provider name. Used in the model's provider identifier and as the key for provider options.

  • url string

    Full URL for the Open Responses API POST endpoint. Pass the endpoint URL, such as http://localhost:1234/v1/responses, not only its base URL.

You can use the following optional settings to customize the Open Responses provider instance:

  • apiKey string

    API key that is sent as a bearer token in the Authorization header.

  • headers Record<string,string> | () => Record<string,string>

    Custom headers to include in requests. Pass a function when headers must be resolved lazily for each request.

  • fetch (input: RequestInfo, init?: RequestInit) => Promise<Response>

    Custom fetch implementation. Defaults to the global fetch function.

  • failedResponseHandler ResponseHandler<APICallError>

    Custom handler for non-successful HTTP responses. Use this when an endpoint returns an error envelope that differs from the default OpenAI-compatible { error: { message, type, param, code } } shape.

  • getResponseErrorMetadata function

    Extracts optional statusCode and isRetryable metadata from a response error object, including endpoint-specific fields. Applies to generation failures and streaming response.failed and error events. The standard Open Responses error fields are code and message; endpoint-specific status fields require this callback. When only a status code is supplied, the SDK derives retryability from it. HTTP failures are handled separately by failedResponseHandler.

  • strictResponseInput boolean

    Serializes assistant history using strict OpenAI Responses input schemas. Assistant messages without an item ID are sent as easy input messages, while messages with a genuine provider item ID are sent as complete output items. Defaults to false.

  • customToolId ${string}.${string}

    Provider-tool ID used by provider.tools.customTool. Defaults to open-responses.custom.

  • structuredOutputs boolean

    Whether JSON response formats are sent to the endpoint. When false, structured-output requests are omitted with an unsupported warning. Defaults to true.

  • userAgentSuffix string

    Overrides the default ai-sdk-open-responses/<version> user-agent suffix.

Endpoint Examples

For a local LM Studio server that does not require authentication:

import { createOpenResponses } from '@ai-sdk/open-responses';

const lmstudio = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

For the OpenAI Responses API:

import { createOpenResponses } from '@ai-sdk/open-responses';

const openAIResponses = createOpenResponses({
  name: 'openai',
  url: 'https://api.openai.com/v1/responses',
  apiKey: process.env.OPENAI_API_KEY,
});

You can use the same setup with another compatible service by changing the name, url, authentication, and model ID.

Language Models

The Open Responses provider instance is a function that you can invoke to create a language model:

const model = openResponses('your-model-id');

The model ID is passed to the endpoint unchanged.

You can use Open Responses models with the generateText and streamText functions, and they support structured data generation with Output (see AI SDK Core).

Generate Text

import { createOpenResponses } from '@ai-sdk/open-responses';
import { generateText } from 'ai';

const openResponses = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

const { text } = await generateText({
  model: openResponses('your-model-id'),
  prompt: 'Invent a new holiday and describe its traditions.',
});

console.log(text);

Stream Text

import { createOpenResponses } from '@ai-sdk/open-responses';
import { streamText } from 'ai';

const openResponses = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

const result = streamText({
  model: openResponses('your-model-id'),
  prompt: 'Invent a new holiday and describe its traditions.',
});

for await (const textPart of result.textStream) {
  process.stdout.write(textPart);
}

Reasoning and Provider Options

Use the top-level reasoning setting to control reasoning effort. The provider maps supported values to the Open Responses reasoning.effort field.

The provider also supports reasoningEffort and reasoningSummary through providerOptions. The provider option key must match the name passed to createOpenResponses:

import {
  createOpenResponses,
  type OpenResponsesLanguageModelOptions,
} from '@ai-sdk/open-responses';
import { generateText } from 'ai';

const lmstudio = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

const { text, reasoningText } = await generateText({
  model: lmstudio('your-reasoning-model-id'),
  reasoning: 'high',
  providerOptions: {
    lmstudio: {
      reasoningEffort: 'max',
      reasoningSummary: 'detailed',
    } satisfies OpenResponsesLanguageModelOptions,
  },
  prompt: 'Explain why the sky appears blue.',
});

console.log(reasoningText);
console.log(text);

reasoningEffort accepts any string and is passed through unchanged to the endpoint's reasoning.effort field. Use it for endpoint-native values that are not part of the top-level reasoning setting, such as 'max'. When both are set, providerOptions reasoningEffort takes precedence over the top-level reasoning value.

reasoningSummary accepts 'auto', 'concise', or 'detailed' and can be combined with either reasoning effort setting. Reasoning support and accepted effort values depend on the endpoint and model; unsupported values may be rejected or ignored by the endpoint.

Custom Tools

Endpoints with OpenAI-compatible custom-tool support can call tools with raw string inputs, optionally constrained by a regex or Lark grammar. These tools execute in your application:

import { generateText, isStepCount } from 'ai';

const result = await generateText({
  model: openResponses('your-model-id'),
  prompt: 'Write a SQL query that lists active users.',
  tools: {
    write_sql: openResponses.tools.customTool({
      description: 'Return a SQL SELECT query.',
      format: {
        type: 'grammar',
        syntax: 'regex',
        definition: 'SELECT .+',
      },
      execute: async sql => runReadOnlyQuery(sql),
    }),
  },
  stopWhen: isStepCount(3),
});

The endpoint must support the OpenAI-compatible custom tool, custom_tool_call, and custom_tool_call_output wire types, as OpenAI and QuiverAI do. These are compatibility extensions, not part of the Open Responses 2026-04-24 specification. Custom-tool results can include text, images, and files when supported by the endpoint.

File Inputs

The provider supports image and non-image file inputs in user messages. Images are sent as input_image parts, while other media types, such as PDFs, are sent as input_file parts.

You can provide a file as inline data:

import { createOpenResponses } from '@ai-sdk/open-responses';
import { readFileSync } from 'node:fs';
import { generateText } from 'ai';

const openResponses = createOpenResponses({
  name: 'lmstudio',
  url: 'http://localhost:1234/v1/responses',
});

const { text } = await generateText({
  model: openResponses('your-file-capable-model-id'),
  messages: [
    {
      role: 'user',
      content: [
        {
          type: 'text',
          text: 'Summarize this document.',
        },
        {
          type: 'file',
          data: readFileSync('./document.pdf'),
          mediaType: 'application/pdf',
          filename: 'document.pdf',
        },
      ],
    },
  ],
});

console.log(text);

You can also set data to a URL. The endpoint and model must support the file's media type. Provider file references, such as OpenAI file IDs, and file data in { type: 'text', text: '...' } format are not supported by this provider.

Experimental Extensions

Open Responses implementations can add namespaced tools, items, and streaming events. Use Experimental_OpenResponsesExtension to encode and decode an implementation's extension wire formats. Unregistered provider tools are omitted from requests with an unsupported warning.

This API is experimental and may change in a future release.

The AI SDK provider-tool ID uses dot notation (acme.document_search), while Open Responses wire types use colon notation (acme:document_search). The namespace in id must match the namespace in every registered wire type.

import {
  createOpenResponses,
  type Experimental_OpenResponsesExtension,
} from '@ai-sdk/open-responses';
import { generateText, tool } from 'ai';
import { z } from 'zod';

const documentSearchExtension: Experimental_OpenResponsesExtension = {
  id: 'acme.document_search',
  toolType: 'acme:document_search',
  itemTypes: ['acme:document_search_receipt'],
  encodeTool: ({ name, args }) => ({
    name,
    index: args.index as string,
  }),

  decodeItem: ({ item }) => [
    {
      type: 'tool-call',
      toolCallId: item.call_id as string,
      toolName: item.name as string,
      input: JSON.stringify(item.query),
      providerExecuted: true,
    },
    {
      type: 'tool-result',
      toolCallId: item.call_id as string,
      toolName: item.name as string,
      result: item.result!,
    },
  ],
};

const acme = createOpenResponses({
  name: 'acme',
  url: 'https://api.acme.example/v1/responses',
  experimental_extensions: [documentSearchExtension],
});

const documentSearch = tool({
  type: 'provider',
  id: 'acme.document_search',
  args: { index: 'documentation' },
  isProviderExecuted: true,
  inputSchema: z.object({ text: z.string() }),
  outputSchema: z.object({
    documents: z.array(
      z.object({
        id: z.string(),
        title: z.string(),
      }),
    ),
  }),
});

const result = await generateText({
  model: acme('your-model-id'),
  prompt: 'Find the extension documentation.',
  tools: { documentSearch },
});

Some Responses-compatible implementations document non-namespaced extension discriminators, such as web_search, web_search_call, or response.custom_tool_call_input.delta. These are not portable Open Responses types. Register each exact value through the dedicated bare fields to opt in:

import type { Experimental_OpenResponsesBareExtension } from '@ai-sdk/open-responses';

const webSearchExtension: Experimental_OpenResponsesBareExtension = {
  id: 'acme.web_search',
  allowBareTypes: true,
  bareToolType: 'web_search',
  bareItemTypes: ['web_search_call'],
  bareEventTypes: ['response.web_search_call.completed'],
  encodeTool: ({ name, args }) => ({
    name,
    search_context_size: args.searchContextSize,
  }),
  decodeItem: ({ item }) => {
    if (
      typeof item.call_id !== 'string' ||
      typeof item.name !== 'string' ||
      typeof item.query !== 'string'
    ) {
      return undefined;
    }

    return [
      {
        type: 'tool-call',
        toolCallId: item.call_id,
        toolName: item.name,
        input: JSON.stringify({ query: item.query }),
        providerExecuted: true,
      },
    ];
  },
  decodeEvent: () => undefined,
};

Set allowBareTypes: true when using bareToolType, bareItemTypes, or bareEventTypes. These fields accept only non-namespaced values. They do not enable unknown JSON passthrough: a bare discriminator is handled only when it exactly matches a registered value. Core Open Responses discriminators and duplicate registrations are rejected. Use the normal namespaced fields whenever the implementation supports them. Experimental_OpenResponsesExtension remains the namespaced interface; use Experimental_OpenResponsesBareExtension only for this compatibility mode.

Set providerExecuted on each decoded tool-call or tool-input-start part. It can vary by item or event.

An extension codec can define:

  • encodeTool: Encodes a provider tool. Returning undefined omits the tool with an unsupported warning. A toolChoice that selects the omitted tool is also omitted.
  • encodeToolChoice: Encodes a specific toolChoice. The default is an object containing the registered toolType or bareToolType.
  • decodeItem: Maps a completed item to AI SDK content parts. mode is either 'generate' or 'stream'.
  • encodeInputItem: Encodes tool-call or tool-result history when the original wire item is unavailable. Returned items must include id, status, and a type registered through itemTypes or bareItemTypes.
  • decodeEvent: Maps a streaming event to AI SDK stream parts. Its state map lasts for one response stream.

Tool, item, and event capabilities are independent. An item-only extension, for example, defines itemTypes (or bareItemTypes) and decodeItem without a tool type or encodeTool. Capability fields must be provided in pairs.

Each decoded item adds an open-responses.extension-replay custom part that stores the original JSON in provider metadata. Other decoded parts reference that custom part instead of copying the item. Passing result.response.messages to a later call replays the original item once, including opaque fields. This also preserves items that decode only to response-only source parts.

The adapter validates only the extension discriminator fields. Validate other item and event fields in the codec before returning AI SDK parts. The original item is stored in provider metadata and may appear in persisted messages, telemetry, logs, or UI payloads.

Extension callbacks cannot be serialized across workflow step boundaries. Create the provider inside the workflow step. Serializing a model configured with extensions throws a SerializationError.

Limitations

  • Stop sequences, topK, and seed are not supported and are ignored with warnings.
  • The provider supports language models only. It does not provide embedding or image generation models.
  • AI SDK function tools and registered Open Responses extensions are supported. Other provider-specific tools and options require a dedicated provider implementation.