UNPKG

@ai-sdk/open-responses

Version:

The **[Open Responses provider](https://ai-sdk.dev/providers/ai-sdk-providers/open-responses)** for the [AI SDK](https://ai-sdk.dev/docs) contains language model support for [Open Responses](https://www.openresponses.org/) compatible APIs.

521 lines (476 loc) 16.2 kB
import { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3GenerateResult, LanguageModelV3StreamPart, LanguageModelV3StreamResult, LanguageModelV3Usage, SharedV3Warning, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, jsonSchema, parseProviderOptions, ParseResult, postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { convertToOpenResponsesInput } from './convert-to-open-responses-input'; import { FunctionToolParam, OpenResponsesRequestBody, OpenResponsesResponseBody, OpenResponsesChunk, openResponsesErrorSchema, ToolChoiceParam, } from './open-responses-api'; import { mapOpenResponsesFinishReason } from './map-open-responses-finish-reason'; import { OpenResponsesConfig } from './open-responses-config'; import { openResponsesOptionsSchema } from './open-responses-options'; export class OpenResponsesLanguageModel implements LanguageModelV3 { readonly specificationVersion = 'v3'; readonly modelId: string; private readonly config: OpenResponsesConfig; constructor(modelId: string, config: OpenResponsesConfig) { this.modelId = modelId; this.config = config; } readonly supportedUrls: Record<string, RegExp[]> = { 'image/*': [/^https?:\/\/.*$/], }; get provider(): string { return this.config.provider; } private async getArgs({ maxOutputTokens, temperature, stopSequences, topP, topK, presencePenalty, frequencyPenalty, seed, prompt, providerOptions, tools, toolChoice, responseFormat, }: LanguageModelV3CallOptions): Promise<{ body: Omit<OpenResponsesRequestBody, 'stream' | 'stream_options'>; warnings: SharedV3Warning[]; }> { const warnings: SharedV3Warning[] = []; if (stopSequences != null) { warnings.push({ type: 'unsupported', feature: 'stopSequences' }); } if (topK != null) { warnings.push({ type: 'unsupported', feature: 'topK' }); } if (seed != null) { warnings.push({ type: 'unsupported', feature: 'seed' }); } const { input, instructions, warnings: inputWarnings, } = await convertToOpenResponsesInput({ prompt, }); warnings.push(...inputWarnings); // Convert function tools to the Open Responses format const functionTools: FunctionToolParam[] | undefined = tools ?.filter(tool => tool.type === 'function') .map(tool => ({ type: 'function' as const, name: tool.name, description: tool.description, parameters: tool.inputSchema, ...(tool.strict != null ? { strict: tool.strict } : {}), })); // Convert tool choice to the Open Responses format const convertedToolChoice: ToolChoiceParam | undefined = toolChoice == null ? undefined : toolChoice.type === 'tool' ? { type: 'function', name: toolChoice.toolName } : toolChoice.type; // 'auto' | 'none' | 'required' const textFormat = responseFormat?.type === 'json' ? { type: 'json_schema' as const, ...(responseFormat.schema != null ? { name: responseFormat.name ?? 'response', description: responseFormat.description, schema: responseFormat.schema, strict: true, } : {}), } : undefined; const openResponsesOptions = await parseProviderOptions({ provider: this.config.providerOptionsName, providerOptions, schema: openResponsesOptionsSchema, }); return { body: { model: this.modelId, input, instructions, max_output_tokens: maxOutputTokens, temperature, top_p: topP, presence_penalty: presencePenalty, frequency_penalty: frequencyPenalty, reasoning: openResponsesOptions?.reasoningEffort != null || openResponsesOptions?.reasoningSummary != null ? { ...(openResponsesOptions?.reasoningEffort != null && { effort: openResponsesOptions.reasoningEffort, }), ...(openResponsesOptions?.reasoningSummary != null && { summary: openResponsesOptions.reasoningSummary, }), } : undefined, tools: functionTools?.length ? functionTools : undefined, tool_choice: convertedToolChoice, ...(textFormat != null && { text: { format: textFormat } }), }, warnings, }; } async doGenerate( options: LanguageModelV3CallOptions, ): Promise<LanguageModelV3GenerateResult> { const { body, warnings } = await this.getArgs(options); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: openResponsesErrorSchema, errorToMessage: error => error.error.message, }), successfulResponseHandler: createJsonResponseHandler( // do not validate the response body, only apply types to the response body jsonSchema<OpenResponsesResponseBody>(() => { throw new Error('json schema not implemented'); }), ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const content: Array<LanguageModelV3Content> = []; let hasToolCalls = false; for (const part of response.output!) { switch (part.type) { // TODO AI SDK 7 adjust reasoning in the specification to better support the reasoning structure from open responses. case 'reasoning': { for (const contentPart of part.content ?? []) { content.push({ type: 'reasoning', text: contentPart.text, }); } break; } case 'message': { for (const contentPart of part.content) { content.push({ type: 'text', text: contentPart.text, }); } break; } case 'function_call': { hasToolCalls = true; content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: part.name, input: part.arguments, }); break; } } } const usage = response.usage; const inputTokens = usage?.input_tokens; const cachedInputTokens = usage?.input_tokens_details?.cached_tokens; const outputTokens = usage?.output_tokens; const reasoningTokens = usage?.output_tokens_details?.reasoning_tokens; return { content, finishReason: { unified: mapOpenResponsesFinishReason({ finishReason: response.incomplete_details?.reason, hasToolCalls, }), raw: response.incomplete_details?.reason ?? undefined, }, usage: { inputTokens: { total: inputTokens, noCache: (inputTokens ?? 0) - (cachedInputTokens ?? 0), cacheRead: cachedInputTokens, cacheWrite: undefined, }, outputTokens: { total: outputTokens, text: (outputTokens ?? 0) - (reasoningTokens ?? 0), reasoning: reasoningTokens, }, raw: response.usage, }, request: { body }, response: { id: response.id, timestamp: new Date(response.created_at! * 1000), modelId: response.model, headers: responseHeaders, body: rawResponse, }, providerMetadata: undefined, warnings, }; } async doStream( options: LanguageModelV3CallOptions, ): Promise<LanguageModelV3StreamResult> { const { body, warnings } = await this.getArgs(options); const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url, headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, stream: true, } satisfies OpenResponsesRequestBody, failedResponseHandler: createJsonErrorResponseHandler({ errorSchema: openResponsesErrorSchema, errorToMessage: error => error.error.message, }), // TODO consider validation successfulResponseHandler: createEventSourceResponseHandler(z.any()), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const usage: LanguageModelV3Usage = { inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined, }, outputTokens: { total: undefined, text: undefined, reasoning: undefined, }, }; const updateUsage = ( responseUsage?: OpenResponsesResponseBody['usage'], ) => { if (!responseUsage) { return; } const inputTokens = responseUsage.input_tokens; const cachedInputTokens = responseUsage.input_tokens_details?.cached_tokens; const outputTokens = responseUsage.output_tokens; const reasoningTokens = responseUsage.output_tokens_details?.reasoning_tokens; usage.inputTokens = { total: inputTokens, noCache: (inputTokens ?? 0) - (cachedInputTokens ?? 0), cacheRead: cachedInputTokens, cacheWrite: undefined, }; usage.outputTokens = { total: outputTokens, text: (outputTokens ?? 0) - (reasoningTokens ?? 0), reasoning: reasoningTokens, }; usage.raw = responseUsage; }; let isActiveReasoning = false; let hasToolCalls = false; let finishReason: LanguageModelV3FinishReason = { unified: 'other', raw: undefined, }; const toolCallsByItemId: Record< string, { toolName?: string; toolCallId?: string; arguments?: string } > = {}; return { stream: response.pipeThrough( new TransformStream< ParseResult<OpenResponsesChunk>, LanguageModelV3StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(parseResult, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: parseResult.rawValue, }); } if (!parseResult.success) { controller.enqueue({ type: 'error', error: parseResult.error }); return; } const chunk = parseResult.value; // Tool call events (single-shot tool-call when complete) if ( chunk.type === 'response.output_item.added' && chunk.item.type === 'function_call' ) { toolCallsByItemId[chunk.item.id] = { toolName: chunk.item.name, toolCallId: chunk.item.call_id, arguments: chunk.item.arguments, }; } else if ( (chunk as { type: string }).type === 'response.function_call_arguments.delta' ) { const functionCallChunk = chunk as { item_id: string; delta: string; }; const toolCall = toolCallsByItemId[functionCallChunk.item_id] ?? (toolCallsByItemId[functionCallChunk.item_id] = {}); toolCall.arguments = (toolCall.arguments ?? '') + functionCallChunk.delta; } else if ( (chunk as { type: string }).type === 'response.function_call_arguments.done' ) { const functionCallChunk = chunk as { item_id: string; arguments: string; }; const toolCall = toolCallsByItemId[functionCallChunk.item_id] ?? (toolCallsByItemId[functionCallChunk.item_id] = {}); toolCall.arguments = functionCallChunk.arguments; } else if ( chunk.type === 'response.output_item.done' && chunk.item.type === 'function_call' ) { const toolCall = toolCallsByItemId[chunk.item.id]; const toolName = toolCall?.toolName ?? chunk.item.name; const toolCallId = toolCall?.toolCallId ?? chunk.item.call_id; const input = toolCall?.arguments ?? chunk.item.arguments ?? ''; controller.enqueue({ type: 'tool-call', toolCallId, toolName, input, }); hasToolCalls = true; delete toolCallsByItemId[chunk.item.id]; } // Reasoning events (note: response.reasoning_text.delta is an LM Studio extension, not in official spec) else if ( chunk.type === 'response.output_item.added' && chunk.item.type === 'reasoning' ) { controller.enqueue({ type: 'reasoning-start', id: chunk.item.id, }); isActiveReasoning = true; } else if ( (chunk as { type: string }).type === 'response.reasoning_text.delta' ) { const reasoningChunk = chunk as { item_id: string; delta: string; }; controller.enqueue({ type: 'reasoning-delta', id: reasoningChunk.item_id, delta: reasoningChunk.delta, }); } else if ( chunk.type === 'response.output_item.done' && chunk.item.type === 'reasoning' ) { controller.enqueue({ type: 'reasoning-end', id: chunk.item.id }); isActiveReasoning = false; } // Text events else if ( chunk.type === 'response.output_item.added' && chunk.item.type === 'message' ) { controller.enqueue({ type: 'text-start', id: chunk.item.id }); } else if (chunk.type === 'response.output_text.delta') { controller.enqueue({ type: 'text-delta', id: chunk.item_id, delta: chunk.delta, }); } else if ( chunk.type === 'response.output_item.done' && chunk.item.type === 'message' ) { controller.enqueue({ type: 'text-end', id: chunk.item.id }); } else if ( chunk.type === 'response.completed' || chunk.type === 'response.incomplete' ) { const reason = chunk.response.incomplete_details?.reason; finishReason = { unified: mapOpenResponsesFinishReason({ finishReason: reason, hasToolCalls, }), raw: reason ?? undefined, }; updateUsage(chunk.response.usage); } else if (chunk.type === 'response.failed') { finishReason = { unified: 'error', raw: chunk.response.error?.code ?? chunk.response.status, }; updateUsage(chunk.response.usage); } }, flush(controller) { if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' }); } controller.enqueue({ type: 'finish', finishReason, usage, providerMetadata: undefined, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } }