diff --git a/.changeset/major-teeth-greet.md b/.changeset/major-teeth-greet.md new file mode 100644 index 000000000..9ac72cb34 --- /dev/null +++ b/.changeset/major-teeth-greet.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-openai': minor +--- + +Introduces a single-source-of-truth model registry for all OpenAI models, preventing silent drift between capability declarations and runtime validation. Significantly expands model coverage across text, image, video, and audio categories. diff --git a/packages/typescript/ai-openai/src/adapters/image.ts b/packages/typescript/ai-openai/src/adapters/image.ts index 585e8a72f..a8d34a1df 100644 --- a/packages/typescript/ai-openai/src/adapters/image.ts +++ b/packages/typescript/ai-openai/src/adapters/image.ts @@ -9,12 +9,12 @@ import { validateNumberOfImages, validatePrompt, } from '../image/image-provider-options' -import type { OpenAIImageModel } from '../model-meta' import type { + OpenAIImageModel, OpenAIImageModelProviderOptionsByName, OpenAIImageModelSizeByName, - OpenAIImageProviderOptions, -} from '../image/image-provider-options' +} from '../model-meta' +import type { OpenAIImageProviderOptions } from '../image/image-provider-options' import type { GeneratedImage, ImageGenerationOptions, @@ -32,7 +32,8 @@ export interface OpenAIImageConfig extends OpenAIClientConfig {} * OpenAI Image Generation Adapter * * Tree-shakeable adapter for OpenAI image generation functionality. - * Supports gpt-image-1, gpt-image-1-mini, dall-e-3, and dall-e-2 models. + * Supports gpt-image-1.5, chatgpt-image-latest, gpt-image-1, gpt-image-1-mini, + * dall-e-3, and dall-e-2 models. * * Features: * - Model-specific type-safe provider options diff --git a/packages/typescript/ai-openai/src/adapters/text.ts b/packages/typescript/ai-openai/src/adapters/text.ts index 1747ce4ec..8e6ea0df9 100644 --- a/packages/typescript/ai-openai/src/adapters/text.ts +++ b/packages/typescript/ai-openai/src/adapters/text.ts @@ -29,7 +29,6 @@ import type { TextOptions, } from '@tanstack/ai' import type { - ExternalTextProviderOptions, InternalTextProviderOptions, } from '../text/text-provider-options' import type { @@ -47,7 +46,9 @@ export interface OpenAITextConfig extends OpenAIClientConfig {} /** * Alias for TextProviderOptions */ -export type OpenAITextProviderOptions = ExternalTextProviderOptions +export type OpenAITextProviderOptions< + TModel extends OpenAIChatModel = OpenAIChatModel, +> = OpenAIChatModelProviderOptionsByName[TModel] // =========================== // Type Resolution Helpers @@ -55,12 +56,9 @@ export type OpenAITextProviderOptions = ExternalTextProviderOptions /** * Resolve provider options for a specific model. - * If the model has explicit options in the map, use those; otherwise use base options. */ -type ResolveProviderOptions = - TModel extends keyof OpenAIChatModelProviderOptionsByName - ? OpenAIChatModelProviderOptionsByName[TModel] - : OpenAITextProviderOptions +type ResolveProviderOptions = + OpenAIChatModelProviderOptionsByName[TModel] /** * Resolve input modalities for a specific model. diff --git a/packages/typescript/ai-openai/src/adapters/transcription.ts b/packages/typescript/ai-openai/src/adapters/transcription.ts index 796bc0b29..275bf1ad3 100644 --- a/packages/typescript/ai-openai/src/adapters/transcription.ts +++ b/packages/typescript/ai-openai/src/adapters/transcription.ts @@ -54,12 +54,14 @@ export class OpenAITranscriptionAdapter< const file = this.prepareAudioFile(audio) // Build request - const request: OpenAI_SDK.Audio.TranscriptionCreateParams = { + const requestBase: Omit< + OpenAI_SDK.Audio.TranscriptionCreateParamsNonStreaming, + 'response_format' + > = { model, file, language, prompt, - response_format: this.mapResponseFormat(responseFormat), ...modelOptions, } @@ -69,9 +71,14 @@ export class OpenAITranscriptionAdapter< (!responseFormat && model !== 'whisper-1') if (useVerbose) { + const verboseRequest: OpenAI_SDK.Audio.TranscriptionCreateParamsNonStreaming<'verbose_json'> = + { + ...requestBase, + response_format: 'verbose_json', + stream: false, + } const response = await this.client.audio.transcriptions.create({ - ...request, - response_format: 'verbose_json', + ...verboseRequest, }) return { @@ -96,12 +103,23 @@ export class OpenAITranscriptionAdapter< })), } } else { + const request: OpenAI_SDK.Audio.TranscriptionCreateParamsNonStreaming = + { + ...requestBase, + response_format: this.mapResponseFormat(responseFormat), + stream: false, + } const response = await this.client.audio.transcriptions.create(request) return { id: generateId(this.name), model, - text: typeof response === 'string' ? response : response.text, + text: + typeof response === 'string' + ? response + : 'text' in response + ? response.text + : '', language, } } @@ -157,9 +175,9 @@ export class OpenAITranscriptionAdapter< private mapResponseFormat( format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt', - ): OpenAI_SDK.Audio.TranscriptionCreateParams['response_format'] { + ): OpenAI_SDK.Audio.AudioResponseFormat { if (!format) return 'json' - return format as OpenAI_SDK.Audio.TranscriptionCreateParams['response_format'] + return format } } diff --git a/packages/typescript/ai-openai/src/adapters/tts.ts b/packages/typescript/ai-openai/src/adapters/tts.ts index 2f34e50fa..5a59d9d5a 100644 --- a/packages/typescript/ai-openai/src/adapters/tts.ts +++ b/packages/typescript/ai-openai/src/adapters/tts.ts @@ -28,7 +28,7 @@ export interface OpenAITTSConfig extends OpenAIClientConfig {} * OpenAI Text-to-Speech Adapter * * Tree-shakeable adapter for OpenAI TTS functionality. - * Supports tts-1, tts-1-hd, and gpt-4o-audio-preview models. + * Supports gpt-4o-mini-tts, tts-1, and tts-1-hd models. * * Features: * - Multiple voice options: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse diff --git a/packages/typescript/ai-openai/src/adapters/video.ts b/packages/typescript/ai-openai/src/adapters/video.ts index 1f882d16d..d03d7ea0a 100644 --- a/packages/typescript/ai-openai/src/adapters/video.ts +++ b/packages/typescript/ai-openai/src/adapters/video.ts @@ -6,12 +6,12 @@ import { validateVideoSize, } from '../video/video-provider-options' import type { VideoModel } from 'openai/resources' -import type { OpenAIVideoModel } from '../model-meta' import type { + OpenAIVideoModel, OpenAIVideoModelProviderOptionsByName, OpenAIVideoModelSizeByName, - OpenAIVideoProviderOptions, -} from '../video/video-provider-options' +} from '../model-meta' +import type { OpenAIVideoProviderOptions } from '../video/video-provider-options' import type { VideoGenerationOptions, VideoJobResult, @@ -53,9 +53,11 @@ export class OpenAIVideoAdapter< readonly name = 'openai' as const private client: OpenAI_SDK + private readonly clientConfig: OpenAIVideoConfig constructor(config: OpenAIVideoConfig, model: TModel) { super(config, model) + this.clientConfig = config this.client = createOpenAIClient(config) } @@ -212,8 +214,9 @@ export class OpenAIVideoAdapter< // Option 3: Return a proxy URL through our server // Let's try fetching and returning a data URL for now - const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1' - const apiKey = this.config.apiKey + const baseUrl = + this.clientConfig.baseURL || 'https://api.openai.com/v1' + const apiKey = this.clientConfig.apiKey const contentResponse = await fetch( `${baseUrl}/videos/${jobId}/content`, diff --git a/packages/typescript/ai-openai/src/audio/audio-provider-options.ts b/packages/typescript/ai-openai/src/audio/audio-provider-options.ts index 6021df5bb..6a014f4ec 100644 --- a/packages/typescript/ai-openai/src/audio/audio-provider-options.ts +++ b/packages/typescript/ai-openai/src/audio/audio-provider-options.ts @@ -1,3 +1,5 @@ +import { TTS_MODELS } from '../models/audio' + export interface AudioProviderOptions { /** * The text to generate audio for. The maximum length is 4096 characters. @@ -46,13 +48,28 @@ export interface AudioProviderOptions { stream_format?: 'sse' | 'audio' } +/** + * Validates the requested stream format against the selected TTS model. + */ export const validateStreamFormat = (options: AudioProviderOptions) => { - const unsupportedModels = ['tts-1', 'tts-1-hd'] - if (options.stream_format && unsupportedModels.includes(options.model)) { + if (!Object.hasOwn(TTS_MODELS, options.model)) { + if (options.stream_format) { + console.warn( + `Unknown TTS model: ${options.model}. stream_format may not be supported.`, + ) + } + return + } + + const modelMeta = TTS_MODELS[options.model as keyof typeof TTS_MODELS] + if (options.stream_format && !modelMeta.supportsStreaming) { throw new Error(`The model ${options.model} does not support streaming.`) } } +/** + * Validates that the requested speech speed falls within OpenAI's supported range. + */ export const validateSpeed = (options: AudioProviderOptions) => { if (options.speed) { if (options.speed < 0.25 || options.speed > 4.0) { @@ -61,13 +78,23 @@ export const validateSpeed = (options: AudioProviderOptions) => { } } +/** + * Validates that the selected TTS model supports voice instructions. + */ export const validateInstructions = (options: AudioProviderOptions) => { - const unsupportedModels = ['tts-1', 'tts-1-hd'] - if (options.instructions && unsupportedModels.includes(options.model)) { + if (!Object.hasOwn(TTS_MODELS, options.model)) { + throw new Error(`Unknown TTS model: ${options.model}`) + } + + const modelMeta = TTS_MODELS[options.model as keyof typeof TTS_MODELS] + if (options.instructions && !modelMeta.supportsInstructions) { throw new Error(`The model ${options.model} does not support instructions.`) } } +/** + * Validates the maximum input length for text-to-speech requests. + */ export const validateAudioInput = (options: AudioProviderOptions) => { if (options.input.length > 4096) { throw new Error('Input text exceeds maximum length of 4096 characters.') diff --git a/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts b/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts index fb7d40b03..0460b2f75 100644 --- a/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts +++ b/packages/typescript/ai-openai/src/audio/transcription-provider-options.ts @@ -14,7 +14,8 @@ export interface OpenAITranscriptionProviderOptions { * Additional information to include in the transcription response. logprobs will return the log probabilities * of the tokens in the response to understand the model's confidence in the transcription. * logprobs only works with response_format set to json and only with the models gpt-4o-transcribe, - * gpt-4o-mini-transcribe, and gpt-4o-mini-transcribe-2025-12-15. + * gpt-4o-mini-transcribe, gpt-4o-mini-transcribe-2025-12-15, and + * gpt-4o-mini-transcribe-2025-03-20. * This field is not supported when using gpt-4o-transcribe-diarize. */ include?: OpenAI.Audio.TranscriptionCreateParams['include'] diff --git a/packages/typescript/ai-openai/src/image/image-provider-options.ts b/packages/typescript/ai-openai/src/image/image-provider-options.ts index a16281cfd..7e5f4627c 100644 --- a/packages/typescript/ai-openai/src/image/image-provider-options.ts +++ b/packages/typescript/ai-openai/src/image/image-provider-options.ts @@ -1,3 +1,5 @@ +import { IMAGE_MODELS } from '../models/image' + /** * OpenAI Image Generation Provider Options * @@ -176,27 +178,6 @@ export type OpenAIImageProviderOptions = | DallE3ProviderOptions | DallE2ProviderOptions -/** - * Type-only map from model name to its specific provider options. - * Used by the core AI types to narrow providerOptions based on the selected model. - */ -export type OpenAIImageModelProviderOptionsByName = { - 'gpt-image-1': GptImage1ProviderOptions - 'gpt-image-1-mini': GptImage1MiniProviderOptions - 'dall-e-3': DallE3ProviderOptions - 'dall-e-2': DallE2ProviderOptions -} - -/** - * Type-only map from model name to its supported sizes. - */ -export type OpenAIImageModelSizeByName = { - 'gpt-image-1': GptImageSize - 'gpt-image-1-mini': GptImageSize - 'dall-e-3': DallE3Size - 'dall-e-2': DallE2Size -} - /** * Internal options interface for validation */ @@ -206,6 +187,14 @@ interface ImageValidationOptions { background?: 'transparent' | 'opaque' | 'auto' | null } +function getImageModelMeta(model: string) { + if (!Object.hasOwn(IMAGE_MODELS, model)) { + throw new Error(`Unknown image model: ${model}`) + } + + return IMAGE_MODELS[model as keyof typeof IMAGE_MODELS] +} + /** * Validates that the provided size is supported by the model. * Throws a descriptive error if the size is not supported. @@ -216,19 +205,10 @@ export function validateImageSize( ): void { if (!size || size === 'auto') return - const validSizes: Record> = { - 'gpt-image-1': ['1024x1024', '1536x1024', '1024x1536', 'auto'], - 'gpt-image-1-mini': ['1024x1024', '1536x1024', '1024x1536', 'auto'], - 'dall-e-3': ['1024x1024', '1792x1024', '1024x1792'], - 'dall-e-2': ['256x256', '512x512', '1024x1024'], - } + const modelMeta = getImageModelMeta(model) + const modelSizes = modelMeta.sizes - const modelSizes = validSizes[model] - if (!modelSizes) { - throw new Error(`Unknown image model: ${model}`) - } - - if (!modelSizes.includes(size)) { + if (!(modelSizes as ReadonlyArray).includes(size)) { throw new Error( `Size "${size}" is not supported by model "${model}". ` + `Supported sizes: ${modelSizes.join(', ')}`, @@ -245,26 +225,22 @@ export function validateNumberOfImages( ): void { if (numberOfImages === undefined) return - // dall-e-3 only supports n=1 - if (model === 'dall-e-3' && numberOfImages !== 1) { - throw new Error( - `Model "dall-e-3" only supports generating 1 image at a time. ` + - `Requested: ${numberOfImages}`, - ) - } + const modelMeta = getImageModelMeta(model) - // Other models support 1-10 - if (numberOfImages < 1 || numberOfImages > 10) { + if (numberOfImages < 1 || numberOfImages > modelMeta.maxImages) { throw new Error( - `Number of images must be between 1 and 10. Requested: ${numberOfImages}`, + `Number of images must be between 1 and ${modelMeta.maxImages}. Requested: ${numberOfImages}`, ) } } +/** + * Validates that the selected image model supports background control. + */ export const validateBackground = (options: ImageValidationOptions) => { - if (options.background) { - const supportedModels = ['gpt-image-1', 'gpt-image-1-mini'] - if (!supportedModels.includes(options.model)) { + if (options.background != null) { + const modelMeta = getImageModelMeta(options.model) + if (!('supportsBackground' in modelMeta)) { throw new Error( `The model ${options.model} does not support background option.`, ) @@ -272,26 +248,17 @@ export const validateBackground = (options: ImageValidationOptions) => { } } +/** + * Validates prompt presence and model-specific prompt length limits. + */ export const validatePrompt = (options: ImageValidationOptions) => { if (options.prompt.length === 0) { throw new Error('Prompt cannot be empty.') } - if ( - (options.model === 'gpt-image-1' || options.model === 'gpt-image-1-mini') && - options.prompt.length > 32000 - ) { - throw new Error( - 'For gpt-image-1/gpt-image-1-mini, prompt length must be less than or equal to 32000 characters.', - ) - } - if (options.model === 'dall-e-2' && options.prompt.length > 1000) { - throw new Error( - 'For dall-e-2, prompt length must be less than or equal to 1000 characters.', - ) - } - if (options.model === 'dall-e-3' && options.prompt.length > 4000) { + const modelMeta = getImageModelMeta(options.model) + if (options.prompt.length > modelMeta.maxPromptLength) { throw new Error( - 'For dall-e-3, prompt length must be less than or equal to 4000 characters.', + `For ${options.model}, prompt length must be less than or equal to ${modelMeta.maxPromptLength} characters.`, ) } } diff --git a/packages/typescript/ai-openai/src/index.ts b/packages/typescript/ai-openai/src/index.ts index afadc4529..d54bcd76c 100644 --- a/packages/typescript/ai-openai/src/index.ts +++ b/packages/typescript/ai-openai/src/index.ts @@ -29,7 +29,6 @@ export { } from './adapters/image' export type { OpenAIImageProviderOptions, - OpenAIImageModelProviderOptionsByName, } from './image/image-provider-options' // Video adapter - for video generation (experimental) @@ -44,9 +43,7 @@ export { } from './adapters/video' export type { OpenAIVideoProviderOptions, - OpenAIVideoModelProviderOptionsByName, OpenAIVideoSize, - // OpenAIVideoDuration, } from './video/video-provider-options' // TTS adapter - for text-to-speech @@ -80,7 +77,11 @@ export type { OpenAIModelInputModalitiesByName, OpenAIChatModel, OpenAIImageModel, + OpenAIImageModelProviderOptionsByName, + OpenAIImageModelSizeByName, OpenAIVideoModel, + OpenAIVideoModelProviderOptionsByName, + OpenAIVideoModelSizeByName, OpenAITTSModel, OpenAITranscriptionModel, } from './model-meta' @@ -90,7 +91,19 @@ export { OPENAI_TRANSCRIPTION_MODELS, OPENAI_VIDEO_MODELS, OPENAI_CHAT_MODELS, + OPENAI_CURRENT_CHAT_MODELS, + OPENAI_DEPRECATED_CHAT_MODELS, + OPENAI_PREVIEW_CHAT_MODELS, + OPENAI_CHAT_SNAPSHOT_MODELS, + OPENAI_CURRENT_IMAGE_MODELS, + OPENAI_IMAGE_SNAPSHOT_MODELS, + OPENAI_CURRENT_TTS_MODELS, + OPENAI_TTS_SNAPSHOT_MODELS, + OPENAI_CURRENT_TRANSCRIPTION_MODELS, + OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS, + OPENAI_CURRENT_VIDEO_MODELS, } from './model-meta' +export { OPENAI_REALTIME_MODELS, OPENAI_REALTIME_SNAPSHOT_MODELS } from './meta/realtime' export type { OpenAITextMetadata, OpenAIImageMetadata, diff --git a/packages/typescript/ai-openai/src/meta/realtime.ts b/packages/typescript/ai-openai/src/meta/realtime.ts new file mode 100644 index 000000000..59211bb47 --- /dev/null +++ b/packages/typescript/ai-openai/src/meta/realtime.ts @@ -0,0 +1,5 @@ +export { + OPENAI_REALTIME_MODELS, + OPENAI_REALTIME_SNAPSHOT_MODELS, + type OpenAIRealtimeModel, +} from '../model-meta' diff --git a/packages/typescript/ai-openai/src/model-meta.ts b/packages/typescript/ai-openai/src/model-meta.ts index 3557a9b41..3624be967 100644 --- a/packages/typescript/ai-openai/src/model-meta.ts +++ b/packages/typescript/ai-openai/src/model-meta.ts @@ -1,2005 +1,76 @@ +import { IMAGE_MODELS } from './models/image' +import { REALTIME_MODELS, TRANSCRIPTION_MODELS, TTS_MODELS } from './models/audio' +import { TEXT_MODELS } from './models/text' +import { VIDEO_MODELS } from './models/video' +import { + idsByStatus, + snapshotIds, + supportedIds, +} from './models/shared' +import type { TextProviderOptionsForEntry } from './models/text' import type { - OpenAIBaseOptions, - OpenAIMetadataOptions, - OpenAIReasoningOptions, - OpenAIReasoningOptionsWithConcise, - OpenAIStreamingOptions, - OpenAIStructuredOutputOptions, - OpenAIToolsOptions, -} from './text/text-provider-options' + RegistryEntryByModel, + RegistryInputByName, + RegistryModelId, + RegistryPropertyByName, + RegistrySizeByName, +} from './models/shared' -interface ModelMeta { - name: string - supports: { - input: Array<'text' | 'image' | 'audio' | 'video'> - output: Array<'text' | 'image' | 'audio' | 'video'> - endpoints: Array< - | 'chat' - | 'chat-completions' - | 'assistants' - | 'speech_generation' - | 'image-generation' - | 'fine-tuning' - | 'batch' - | 'image-edit' - | 'moderation' - | 'translation' - | 'realtime' - | 'audio' - | 'video' - | 'transcription' - > - features: Array< - | 'streaming' - | 'function_calling' - | 'structured_outputs' - | 'predicted_outcomes' - | 'distillation' - | 'fine_tuning' - > - tools?: Array< - | 'web_search' - | 'file_search' - | 'image_generation' - | 'code_interpreter' - | 'mcp' - | 'computer_use' - > - } - context_window?: number - max_output_tokens?: number - knowledge_cutoff?: string - pricing: { - input: { - normal: number - cached?: number - } - output: { - normal: number - } - } - /** - * Type-level description of which provider options this model supports. - */ - providerOptions?: TProviderOptions -} - -const GPT5_2 = { - name: 'gpt-5.2', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2025-08-31', - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - ], - tools: [ - 'web_search', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, - pricing: { - input: { - normal: 1.75, - cached: 0.175, - }, - output: { - normal: 14, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> -const GPT5_2_PRO = { - name: 'gpt-5.2-pro', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2025-08-31', - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['streaming', 'function_calling'], - tools: ['web_search', 'file_search', 'image_generation', 'mcp'], - }, - pricing: { - input: { - normal: 21, - }, - output: { - normal: 168, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_2_CHAT = { - name: 'gpt-5.2-chat-latest', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2025-08-31', - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['streaming', 'function_calling', 'structured_outputs'], - tools: [], - }, - pricing: { - input: { - normal: 1.75, - cached: 0.175, - }, - output: { - normal: 14, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> -const GPT5_1 = { - name: 'gpt-5.1', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-09-30', - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - endpoints: ['chat', 'chat-completions'], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - ], - tools: [ - 'web_search', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_1_CODEX = { - name: 'gpt-5.1-codex', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-09-30', - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - endpoints: ['chat'], - features: ['streaming', 'function_calling', 'structured_outputs'], - }, - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5 = { - name: 'gpt-5', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-09-30', - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions', 'batch'], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - ], - tools: [ - 'web_search', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_MINI = { - name: 'gpt-5-mini', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-05-31', - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions', 'batch'], - features: ['streaming', 'structured_outputs', 'function_calling'], - tools: ['web_search', 'file_search', 'mcp', 'code_interpreter'], - }, - pricing: { - input: { - normal: 0.25, - cached: 0.025, - }, - output: { - normal: 2, - }, - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_NANO = { - name: 'gpt-5-nano', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-05-31', - pricing: { - input: { - normal: 0.05, - cached: 0.005, - }, - output: { - normal: 0.4, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions', 'batch'], - features: ['streaming', 'structured_outputs', 'function_calling'], - tools: [ - 'web_search', - 'file_search', - 'mcp', - 'image_generation', - 'code_interpreter', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_PRO = { - name: 'gpt-5-pro', - context_window: 400_000, - max_output_tokens: 272_000, - knowledge_cutoff: '2024-09-30', - pricing: { - input: { - normal: 15, - }, - output: { - normal: 120, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['streaming', 'structured_outputs', 'function_calling'], - tools: ['web_search', 'file_search', 'image_generation', 'mcp'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT5_CODEX = { - name: 'gpt-5-codex', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-09-30', - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - endpoints: ['chat'], - features: ['streaming', 'structured_outputs', 'function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -/** - * Sora-2 video generation model. - * @experimental Video generation is an experimental feature and may change. - */ -const SORA2 = { - name: 'sora-2', - pricing: { - input: { - normal: 0, - }, - output: { - // per second of video - normal: 0.1, - }, - }, - supports: { - input: ['text', 'image'], - output: ['video', 'audio'], - endpoints: ['video'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -/** - * Sora-2-Pro video generation model (higher quality). - * @experimental Video generation is an experimental feature and may change. - */ -const SORA2_PRO = { - name: 'sora-2-pro', - pricing: { - input: { - normal: 0, - }, - output: { - // per second of video - normal: 0.5, - }, - }, - supports: { - input: ['text', 'image'], - output: ['video', 'audio'], - endpoints: ['video'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const GPT_IMAGE_1 = { - name: 'gpt-image-1', - // todo fix for images - pricing: { - input: { - normal: 5, - cached: 1.25, - }, - output: { - normal: 0.1, - }, - }, - supports: { - input: ['text', 'image'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const GPT_IMAGE_1_MINI = { - name: 'gpt-image-1-mini', - // todo fix for images - pricing: { - input: { - normal: 2, - cached: 0.2, - }, - output: { - normal: 0.03, - }, - }, - supports: { - input: ['text', 'image'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const O3_DEEP_RESEARCH = { - name: 'o3-deep-research', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 10, - cached: 2.5, - }, - output: { - normal: 40, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const O4_MINI_DEEP_RESEARCH = { - name: 'o4-mini-deep-research', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 2, - cached: 0.5, - }, - output: { - normal: 8, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const O3_PRO = { - name: 'o3-pro', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 20, - }, - output: { - normal: 80, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['function_calling', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_AUDIO = { - name: 'gpt-audio', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio tokens to input output - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -/* const GPT_REALTIME = { - name: 'gpt-realtime', - context_window: 32_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio tokens to input output - input: { - normal: 4, - cached: 0.5, - }, - output: { - normal: 16, - }, - }, - supports: { - input: ['text', 'audio', 'image'], - output: ['text', 'audio'], - endpoints: ['realtime'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_REALTIME_MINI = { - name: 'gpt-realtime-mini', - context_window: 32_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio and image tokens to input output - input: { - normal: 0.6, - cached: 0.06, - }, - output: { - normal: 2.4, - }, - }, - supports: { - input: ['text', 'audio', 'image'], - output: ['text', 'audio'], - endpoints: ['realtime'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> */ - -const GPT_AUDIO_MINI = { - name: 'gpt-audio-mini', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio tokens to input output - input: { - normal: 0.6, - }, - output: { - normal: 2.4, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const O3 = { - name: 'o3', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 2, - cached: 0.5, - }, - output: { - normal: 8, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch', 'chat-completions'], - features: ['function_calling', 'structured_outputs', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const O4_MINI = { - name: 'o4-mini', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 1.1, - cached: 0.275, - }, - output: { - normal: 4.4, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch', 'chat-completions', 'fine-tuning'], - features: [ - 'function_calling', - 'structured_outputs', - 'streaming', - 'fine_tuning', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT4_1 = { - name: 'gpt-4.1', - context_window: 1_047_576, - max_output_tokens: 32_768, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 2, - cached: 0.5, - }, - output: { - normal: 8, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'assistants', - 'fine-tuning', - 'batch', - ], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - 'fine_tuning', - ], - tools: [ - 'web_search', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT4_1_MINI = { - name: 'gpt-4.1-mini', - context_window: 1_047_576, - max_output_tokens: 32_768, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 0.4, - cached: 0.1, - }, - output: { - normal: 1.6, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'assistants', - 'fine-tuning', - 'batch', - ], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'fine_tuning', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT4_1_NANO = { - name: 'gpt-4.1-nano', - context_window: 1_047_576, - max_output_tokens: 32_768, - knowledge_cutoff: '2024-01-01', - pricing: { - input: { - normal: 0.1, - cached: 0.025, - }, - output: { - normal: 0.4, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'assistants', - 'fine-tuning', - 'batch', - ], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'fine_tuning', - 'predicted_outcomes', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const O1_PRO = { - name: 'o1-pro', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 150, - }, - output: { - normal: 600, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['function_calling', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const COMPUTER_USE_PREVIEW = { - name: 'computer-use-preview', - context_window: 8_192, - max_output_tokens: 1_024, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 3, - }, - output: { - normal: 12, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptionsWithConcise & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_4O_MINI_SEARCH_PREVIEW = { - name: 'gpt-4o-mini-search-preview', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 0.15, - }, - output: { - normal: 0.6, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat-completions'], - features: ['streaming', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> +export const OPENAI_CHAT_MODELS = supportedIds(TEXT_MODELS) +export const OPENAI_CURRENT_CHAT_MODELS = idsByStatus(TEXT_MODELS, 'active') +export const OPENAI_DEPRECATED_CHAT_MODELS = idsByStatus(TEXT_MODELS, 'deprecated') +export const OPENAI_PREVIEW_CHAT_MODELS = idsByStatus(TEXT_MODELS, 'preview') +export const OPENAI_CHAT_SNAPSHOT_MODELS = snapshotIds(TEXT_MODELS) -const GPT_4O_SEARCH_PREVIEW = { - name: 'gpt-4o-search-preview', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat-completions'], - features: ['streaming', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const O3_MINI = { - name: 'o3-mini', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 1.1, - cached: 0.55, - }, - output: { - normal: 4.4, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat', 'batch', 'chat-completions', 'assistants'], - features: ['function_calling', 'structured_outputs', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_4O_MINI_AUDIO = { - name: 'gpt-4o-mini-audio', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo audio tokens - input: { - normal: 0.15, - }, - output: { - normal: 0.6, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['function_calling', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -/* const GPT_4O_MINI_REALTIME = { - name: 'gpt-4o-mini-realtime', - context_window: 16_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio tokens - input: { - normal: 0.6, - cached: 0.3, - }, - output: { - normal: 2.4, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['realtime'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - */ -const O1 = { - name: 'o1', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 15, - cached: 7.5, - }, - output: { - normal: 60, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch', 'chat-completions', 'assistants'], - features: ['function_calling', 'structured_outputs', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -/* const OMNI_MODERATION = { - name: 'omni-moderation', - pricing: { - input: { - normal: 0, - }, - output: { - normal: 0, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['batch', 'moderation'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ - -const GPT_4O = { - name: 'gpt-4o', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 2.5, - cached: 1.25, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'assistants', - 'fine-tuning', - 'batch', - ], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - 'fine_tuning', - 'predicted_outcomes', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_4O_AUDIO = { - name: 'gpt-4o-audio', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo audio tokens - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['streaming', 'function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const GPT_4O_MINI = { - name: 'gpt-4o-mini', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 0.15, - cached: 0.075, - }, - output: { - normal: 0.6, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'assistants', - 'fine-tuning', - 'batch', - ], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'fine_tuning', - 'predicted_outcomes', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> +export type OpenAIChatModel = RegistryModelId -/* const GPT__4O_REALTIME = { - name: 'gpt-4o-realtime', - context_window: 32_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo add audio tokens to input output - input: { - normal: 5, - cached: 2.5, - }, - output: { - normal: 20, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['realtime'], - features: ['function_calling'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> */ - -const GPT_4_TURBO = { - name: 'gpt-4-turbo', - context_window: 128_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-12-01', - pricing: { - input: { - normal: 10, - }, - output: { - normal: 30, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions', 'assistants', 'batch'], - features: ['function_calling', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const CHATGPT_40 = { - name: 'chatgpt-4o-latest', - context_window: 128_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 5, - }, - output: { - normal: 15, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['predicted_outcomes', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const GPT_5_1_CODEX_MINI = { - name: 'gpt-5.1-codex-mini', - context_window: 400_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2024-09-30', - pricing: { - input: { - normal: 0.25, - cached: 0.025, - }, - output: { - normal: 2, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text', 'image'], - endpoints: ['chat'], - features: ['streaming', 'function_calling', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const CODEX_MINI_LATEST = { - name: 'codex-mini-latest', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-06-01', - pricing: { - input: { - normal: 1.5, - cached: 0.375, - }, - output: { - normal: 6, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'function_calling', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const DALL_E_2 = { - name: 'dall-e-2', - pricing: { - // todo image tokens - input: { - normal: 0.016, - }, - output: { - normal: 0.02, - }, - }, - supports: { - input: ['text'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const DALL_E_3 = { - name: 'dall-e-3', - pricing: { - // todo image tokens - input: { - normal: 0.04, - }, - output: { - normal: 0.08, - }, - }, - supports: { - input: ['text'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> +export type OpenAIChatModelProviderOptionsByName = { + [TModel in OpenAIChatModel]: TextProviderOptionsForEntry< + RegistryEntryByModel + > +} -const GPT_3_5_TURBO = { - name: 'gpt-3.5-turbo', - context_window: 16_385, - max_output_tokens: 4_096, - knowledge_cutoff: '2021-09-01', - pricing: { - input: { - normal: 0.5, - }, - output: { - normal: 1.5, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat', 'chat-completions', 'batch', 'fine-tuning'], - features: ['fine_tuning'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions +export type OpenAIModelInputModalitiesByName = RegistryInputByName< + typeof TEXT_MODELS > -const GPT_4 = { - name: 'gpt-4', - context_window: 8_192, - max_output_tokens: 8_192, - knowledge_cutoff: '2023-12-01', - pricing: { - input: { - normal: 30, - }, - output: { - normal: 60, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: [ - 'chat', - 'chat-completions', - 'batch', - 'fine-tuning', - 'assistants', - ], - features: ['fine_tuning', 'streaming'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> -/* -const GPT_4O_MINI_TRANSCRIBE = { - name: 'gpt-4o-mini-transcribe', - context_window: 16_000, - max_output_tokens: 2_000, - knowledge_cutoff: '2024-01-01', - pricing: { - // todo audio tokens - input: { - normal: 1.25, - }, - output: { - normal: 5, - }, - }, - supports: { - input: ['audio', 'text'], - output: ['text'], - endpoints: ['realtime', 'transcription'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> +export const OPENAI_IMAGE_MODELS = supportedIds(IMAGE_MODELS) +export const OPENAI_CURRENT_IMAGE_MODELS = idsByStatus(IMAGE_MODELS, 'active') +export const OPENAI_IMAGE_SNAPSHOT_MODELS = snapshotIds(IMAGE_MODELS) -const GPT_4O_MINI_TTS = { - name: 'gpt-4o-mini-tts', - pricing: { - // todo audio tokens - input: { - normal: 0.6, - }, - output: { - normal: 12, - }, - }, - supports: { - input: ['text'], - output: ['audio'], - endpoints: ['speech_generation'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions +export type OpenAIImageModel = RegistryModelId +export type OpenAIImageModelProviderOptionsByName = RegistryPropertyByName< + typeof IMAGE_MODELS, + 'providerOptions' > +export type OpenAIImageModelSizeByName = RegistrySizeByName -const GPT_4O_TRANSCRIBE = { - name: 'gpt-4o-transcribe', - context_window: 16_000, - max_output_tokens: 2_000, - knowledge_cutoff: '2024-06-01', - pricing: { - // todo audio tokens - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['audio', 'text'], - output: ['text'], - endpoints: ['realtime', 'transcription'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ -/* -const GPT_4O_TRANSCRIBE_DIARIZE = { - name: 'gpt-4o-transcribe-diarize', - context_window: 16_000, - max_output_tokens: 2_000, - knowledge_cutoff: '2024-06-01', - pricing: { - // todo audio tokens - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['audio', 'text'], - output: ['text'], - endpoints: ['transcription'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ +export const OPENAI_VIDEO_MODELS = supportedIds(VIDEO_MODELS) +export const OPENAI_CURRENT_VIDEO_MODELS = idsByStatus(VIDEO_MODELS, 'active') -const GPT_5_1_CHAT = { - name: 'gpt-5.1-chat-latest', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2024-09-30', - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['streaming', 'function_calling', 'structured_outputs'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions +export type OpenAIVideoModel = RegistryModelId +export type OpenAIVideoModelProviderOptionsByName = RegistryPropertyByName< + typeof VIDEO_MODELS, + 'providerOptions' > +export type OpenAIVideoModelSizeByName = RegistrySizeByName -const GPT_5_CHAT = { - name: 'gpt-5-chat-latest', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2024-09-30', - pricing: { - input: { - normal: 1.25, - cached: 0.125, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['streaming', 'function_calling', 'structured_outputs'], - tools: [ - 'web_search', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> +export const OPENAI_TTS_MODELS = supportedIds(TTS_MODELS) +export const OPENAI_CURRENT_TTS_MODELS = idsByStatus(TTS_MODELS, 'active') +export const OPENAI_TTS_SNAPSHOT_MODELS = snapshotIds(TTS_MODELS) +export type OpenAITTSModel = RegistryModelId -/* const TTS_1 = { - name: 'tts-1', - pricing: { - // todo figure out pricing - input: { - normal: 15, - }, - output: { - normal: 15, - }, - }, - supports: { - input: ['text'], - output: ['audio'], - endpoints: ['speech_generation'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions +export const OPENAI_TRANSCRIPTION_MODELS = supportedIds(TRANSCRIPTION_MODELS) +export const OPENAI_CURRENT_TRANSCRIPTION_MODELS = idsByStatus( + TRANSCRIPTION_MODELS, + 'active', +) +export const OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS = + snapshotIds(TRANSCRIPTION_MODELS) +export type OpenAITranscriptionModel = RegistryModelId< + typeof TRANSCRIPTION_MODELS > -const TTS_1_HD = { - name: 'tts-1-hd', - pricing: { - // todo figure out pricing - input: { - normal: 30, - }, - output: { - normal: 30, - }, - }, - supports: { - input: ['text'], - output: ['audio'], - endpoints: ['speech_generation'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> */ - -// Chat/text completion models (based on endpoints: "chat" or "chat-completions") -export const OPENAI_CHAT_MODELS = [ - // Frontier models - GPT5_2.name, - GPT5_2_PRO.name, - GPT5_2_CHAT.name, - GPT5_1.name, - GPT5_1_CODEX.name, - GPT5.name, - GPT5_MINI.name, - GPT5_NANO.name, - GPT5_PRO.name, - GPT5_CODEX.name, - // Reasoning models - O3.name, - O3_PRO.name, - O3_MINI.name, - O4_MINI.name, - O3_DEEP_RESEARCH.name, - O4_MINI_DEEP_RESEARCH.name, - // GPT-4 series - GPT4_1.name, - GPT4_1_MINI.name, - GPT4_1_NANO.name, - GPT_4.name, - GPT_4_TURBO.name, - GPT_4O.name, - GPT_4O_MINI.name, - // GPT-3.5 - GPT_3_5_TURBO.name, - // Audio-enabled chat models - GPT_AUDIO.name, - GPT_AUDIO_MINI.name, - GPT_4O_AUDIO.name, - GPT_4O_MINI_AUDIO.name, - // ChatGPT models - GPT_5_1_CHAT.name, - GPT_5_CHAT.name, - CHATGPT_40.name, - // Specialized - GPT_5_1_CODEX_MINI.name, - CODEX_MINI_LATEST.name, - // Preview models - GPT_4O_SEARCH_PREVIEW.name, - GPT_4O_MINI_SEARCH_PREVIEW.name, - COMPUTER_USE_PREVIEW.name, - // Legacy reasoning - O1.name, - O1_PRO.name, -] as const - -export type OpenAIChatModel = (typeof OPENAI_CHAT_MODELS)[number] - -// Image generation models (based on endpoints: "image-generation" or "image-edit") -export const OPENAI_IMAGE_MODELS = [ - GPT_IMAGE_1.name, - GPT_IMAGE_1_MINI.name, - DALL_E_3.name, - DALL_E_2.name, -] as const - -export type OpenAIImageModel = (typeof OPENAI_IMAGE_MODELS)[number] - -// Audio models (based on endpoints: "transcription", "speech_generation", or "realtime") -/* const OPENAI_AUDIO_MODELS = [ - // Transcription models - GPT_4O_TRANSCRIBE.name, - GPT_4O_TRANSCRIBE_DIARIZE.name, - GPT_4O_MINI_TRANSCRIBE.name, - // Realtime models - GPT_REALTIME.name, - GPT_REALTIME_MINI.name, - GPT__4O_REALTIME.name, - GPT_4O_MINI_REALTIME.name, - // Text-to-speech models - GPT_4O_MINI_TTS.name, - TTS_1.name, - TTS_1_HD.name, -] as const */ - -// Transcription-only models (based on endpoints: "transcription") -/* const OPENAI_TRANSCRIPTION_MODELS = [ - GPT_4O_TRANSCRIBE.name, - GPT_4O_TRANSCRIBE_DIARIZE.name, - GPT_4O_MINI_TRANSCRIBE.name, -] as const - -/** - * Video generation models (based on endpoints: "video") - * @experimental Video generation is an experimental feature and may change. - */ -export const OPENAI_VIDEO_MODELS = [SORA2.name, SORA2_PRO.name] as const - -export type OpenAIVideoModel = (typeof OPENAI_VIDEO_MODELS)[number] - -/** - * Text-to-speech models (based on endpoints: "speech_generation") - */ -export const OPENAI_TTS_MODELS = [ - 'tts-1', - 'tts-1-hd', - 'gpt-4o-audio-preview', -] as const - -export type OpenAITTSModel = (typeof OPENAI_TTS_MODELS)[number] - -/** - * Transcription models (based on endpoints: "transcription") - */ -export const OPENAI_TRANSCRIPTION_MODELS = [ - 'whisper-1', - 'gpt-4o-transcribe', - 'gpt-4o-mini-transcribe', - 'gpt-4o-transcribe-diarize', -] as const - -export type OpenAITranscriptionModel = - (typeof OPENAI_TRANSCRIPTION_MODELS)[number] - -/** - * Type-only map from chat model name to its provider options type. - * Used by the core AI types (via the adapter) to narrow - * `providerOptions` based on the selected model. - * - * Manually defined to ensure accurate type narrowing per model. - */ -export type OpenAIChatModelProviderOptionsByName = { - [GPT5_2.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_2_CHAT.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_2_PRO.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_1.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_1_CODEX.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_MINI.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_NANO.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_PRO.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT5_CODEX.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT4_1.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT4_1_MINI.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT4_1_NANO.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O_MINI.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - - // Models WITHOUT structured output support (NO 'text' field) - [GPT_4.name]: OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4_TURBO.name]: OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_3_5_TURBO.name]: OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [CHATGPT_40.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [O3.name]: OpenAIBaseOptions & OpenAIReasoningOptions & OpenAIMetadataOptions - [O3_PRO.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - [O3_MINI.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - [O4_MINI.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - [O3_DEEP_RESEARCH.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - [O4_MINI_DEEP_RESEARCH.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - [O1.name]: OpenAIBaseOptions & OpenAIReasoningOptions & OpenAIMetadataOptions - [O1_PRO.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIMetadataOptions - - // Audio models - [GPT_AUDIO.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_AUDIO_MINI.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O_AUDIO.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O_MINI_AUDIO.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - - // Chat-only models - [GPT_5_1_CHAT.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIMetadataOptions - [GPT_5_CHAT.name]: OpenAIBaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIMetadataOptions - - // Codex models - [GPT_5_1_CODEX_MINI.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [CODEX_MINI_LATEST.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - - // Search models - [GPT_4O_SEARCH_PREVIEW.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O_MINI_SEARCH_PREVIEW.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - - // Special models - [COMPUTER_USE_PREVIEW.name]: OpenAIBaseOptions & - OpenAIReasoningOptionsWithConcise & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -} - -/** - * Type-only map from chat model name to its supported input modalities. - * Based on the 'supports.input' arrays defined for each model. - * Used by the core AI types to constrain ContentPart types based on the selected model. - * Note: These must be inlined as readonly arrays (not typeof) because the model - * constants are not exported and typeof references don't work in .d.ts files - * when consumed by external packages. - */ -export type OpenAIModelInputModalitiesByName = { - [GPT5_2.name]: typeof GPT5_2.supports.input - [GPT5_2_PRO.name]: typeof GPT5_2_PRO.supports.input - [GPT5_2_CHAT.name]: typeof GPT5_2_CHAT.supports.input - // Models with text + image input - [GPT5_1.name]: typeof GPT5_1.supports.input - [GPT5_1_CODEX.name]: typeof GPT5_1_CODEX.supports.input - [GPT5.name]: typeof GPT5.supports.input - [GPT5_MINI.name]: typeof GPT5_MINI.supports.input - [GPT5_NANO.name]: typeof GPT5_NANO.supports.input - [GPT5_PRO.name]: typeof GPT5_PRO.supports.input - [GPT5_CODEX.name]: typeof GPT5_CODEX.supports.input - [GPT4_1.name]: typeof GPT4_1.supports.input - [GPT4_1_MINI.name]: typeof GPT4_1_MINI.supports.input - [GPT4_1_NANO.name]: typeof GPT4_1_NANO.supports.input - - [GPT_4O.name]: typeof GPT_4O.supports.input - [GPT_4O_MINI.name]: typeof GPT_4O_MINI.supports.input - [GPT_4_TURBO.name]: typeof GPT_4_TURBO.supports.input - [CHATGPT_40.name]: typeof CHATGPT_40.supports.input - [GPT_5_1_CHAT.name]: typeof GPT_5_1_CHAT.supports.input - [GPT_5_CHAT.name]: typeof GPT_5_CHAT.supports.input - [GPT_5_1_CODEX_MINI.name]: typeof GPT_5_1_CODEX_MINI.supports.input - [CODEX_MINI_LATEST.name]: typeof CODEX_MINI_LATEST.supports.input - [COMPUTER_USE_PREVIEW.name]: typeof COMPUTER_USE_PREVIEW.supports.input - [O3.name]: typeof O3.supports.input - [O3_PRO.name]: typeof O3_PRO.supports.input - [O3_DEEP_RESEARCH.name]: typeof O3_DEEP_RESEARCH.supports.input - [O4_MINI_DEEP_RESEARCH.name]: typeof O4_MINI_DEEP_RESEARCH.supports.input - [O4_MINI.name]: typeof O4_MINI.supports.input - [O1.name]: typeof O1.supports.input - [O1_PRO.name]: typeof O1_PRO.supports.input - - // Models with text + audio input - [GPT_AUDIO.name]: typeof GPT_AUDIO.supports.input - [GPT_AUDIO_MINI.name]: typeof GPT_AUDIO_MINI.supports.input - [GPT_4O_AUDIO.name]: typeof GPT_4O_AUDIO.supports.input - [GPT_4O_MINI_AUDIO.name]: typeof GPT_4O_MINI_AUDIO.supports.input - - // Text-only models - [GPT_4.name]: typeof GPT_4.supports.input - [GPT_3_5_TURBO.name]: typeof GPT_3_5_TURBO.supports.input - [O3_MINI.name]: typeof O3_MINI.supports.input - [GPT_4O_SEARCH_PREVIEW.name]: typeof GPT_4O_SEARCH_PREVIEW.supports.input - [GPT_4O_MINI_SEARCH_PREVIEW.name]: typeof GPT_4O_MINI_SEARCH_PREVIEW.supports.input -} +export const OPENAI_REALTIME_MODELS = supportedIds(REALTIME_MODELS) +export const OPENAI_REALTIME_SNAPSHOT_MODELS = snapshotIds(REALTIME_MODELS) +export type OpenAIRealtimeModel = RegistryModelId diff --git a/packages/typescript/ai-openai/src/models/audio.ts b/packages/typescript/ai-openai/src/models/audio.ts new file mode 100644 index 000000000..8fe52d7b8 --- /dev/null +++ b/packages/typescript/ai-openai/src/models/audio.ts @@ -0,0 +1,165 @@ +import type { OpenAIRegistryDocs } from './shared' + +interface TTSModelSpec { + input: readonly ['text'] + output: readonly ['audio'] + snapshots?: ReadonlyArray + lifecycle: { + status: 'active' | 'legacy' + replacedBy?: string + } + supportsStreaming: boolean + supportsInstructions: boolean + docs?: OpenAIRegistryDocs +} + +interface TranscriptionModelSpec { + input: readonly ['audio', 'text'] + output: readonly ['text'] + snapshots?: ReadonlyArray + lifecycle: { + status: 'active' | 'legacy' + replacedBy?: string + } + supportsLogprobs: boolean + supportsDiarization: boolean + docs?: OpenAIRegistryDocs +} + +interface RealtimeModelSpec { + input: readonly ['text', 'audio', 'image'] + output: readonly ['text', 'audio'] + snapshots?: ReadonlyArray + lifecycle: { + status: 'active' | 'preview' | 'deprecated' | 'legacy' + replacedBy?: string + } + docs?: OpenAIRegistryDocs +} + +export const TTS_MODELS = { + 'gpt-4o-mini-tts': { + input: ['text'], + output: ['audio'], + lifecycle: { status: 'active' }, + supportsStreaming: true, + supportsInstructions: true, + snapshots: ['gpt-4o-mini-tts-2025-12-15', 'gpt-4o-mini-tts-2025-03-20'], + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-mini-tts', + billing: { input: 0.6, output: 12 }, + }, + }, + 'tts-1': { + input: ['text'], + output: ['audio'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-4o-mini-tts' }, + supportsStreaming: false, + supportsInstructions: false, + }, + 'tts-1-hd': { + input: ['text'], + output: ['audio'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-4o-mini-tts' }, + supportsStreaming: false, + supportsInstructions: false, + }, +} as const satisfies Record + +export const TRANSCRIPTION_MODELS = { + 'whisper-1': { + input: ['audio', 'text'], + output: ['text'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-4o-transcribe' }, + supportsLogprobs: false, + supportsDiarization: false, + docs: { + source: 'https://developers.openai.com/api/docs/models/whisper-1', + }, + }, + 'gpt-4o-transcribe': { + input: ['audio', 'text'], + output: ['text'], + lifecycle: { status: 'active' }, + supportsLogprobs: true, + supportsDiarization: false, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-transcribe', + }, + }, + 'gpt-4o-mini-transcribe': { + input: ['audio', 'text'], + output: ['text'], + lifecycle: { status: 'active' }, + supportsLogprobs: true, + supportsDiarization: false, + snapshots: [ + 'gpt-4o-mini-transcribe-2025-12-15', + 'gpt-4o-mini-transcribe-2025-03-20', + ], + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-mini-transcribe', + }, + }, + 'gpt-4o-transcribe-diarize': { + input: ['audio', 'text'], + output: ['text'], + lifecycle: { status: 'active' }, + supportsLogprobs: false, + supportsDiarization: true, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-transcribe-diarize', + }, + }, +} as const satisfies Record + +export const REALTIME_MODELS = { + 'gpt-realtime-1.5': { + input: ['text', 'audio', 'image'], + output: ['text', 'audio'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-realtime-1.5', + limits: { + contextWindow: 32_768, + maxOutputTokens: 4_096, + knowledgeCutoff: '2024-06-01', + }, + billing: { + audio: { + input: 4, + cachedInput: 0.4, + output: 16, + }, + }, + }, + }, + 'gpt-realtime': { + input: ['text', 'audio', 'image'], + output: ['text', 'audio'], + snapshots: ['gpt-realtime-2025-08-28'], + lifecycle: { status: 'active' }, + }, + 'gpt-realtime-mini': { + input: ['text', 'audio', 'image'], + output: ['text', 'audio'], + snapshots: ['gpt-realtime-mini-2025-12-15', 'gpt-realtime-mini-2025-10-06'], + lifecycle: { status: 'active' }, + }, + 'gpt-4o-realtime-preview': { + input: ['text', 'audio', 'image'], + output: ['text', 'audio'], + snapshots: [ + 'gpt-4o-realtime-preview-2025-06-03', + 'gpt-4o-realtime-preview-2024-12-17', + 'gpt-4o-realtime-preview-2024-10-01', + ], + lifecycle: { status: 'preview' }, + }, + 'gpt-4o-mini-realtime-preview': { + input: ['text', 'audio', 'image'], + output: ['text', 'audio'], + snapshots: ['gpt-4o-mini-realtime-preview-2024-12-17'], + lifecycle: { status: 'preview' }, + }, +} as const satisfies Record diff --git a/packages/typescript/ai-openai/src/models/image.ts b/packages/typescript/ai-openai/src/models/image.ts new file mode 100644 index 000000000..b6963633b --- /dev/null +++ b/packages/typescript/ai-openai/src/models/image.ts @@ -0,0 +1,123 @@ +import type { + DallE2ProviderOptions, + DallE3ProviderOptions, + GptImage1MiniProviderOptions, + GptImage1ProviderOptions, +} from '../image/image-provider-options' +import type { OpenAIRegistryDocs } from './shared' + +interface ImageModelSpec< + TProviderOptions, + TSize extends string, + TInput extends ReadonlyArray<'text' | 'image'>, +> { + input: TInput + output: readonly ['image'] + providerOptions: TProviderOptions + sizes: ReadonlyArray + supportsBackground?: boolean + maxImages: number + maxPromptLength: number + snapshots?: ReadonlyArray + lifecycle: { + status: 'active' | 'deprecated' | 'legacy' | 'chatgpt_only' + replacedBy?: string + } + docs?: OpenAIRegistryDocs +} + +export const IMAGE_MODELS = { + 'gpt-image-1.5': { + input: ['text', 'image'], + output: ['image'], + providerOptions: {} as GptImage1ProviderOptions, + sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'], + supportsBackground: true, + maxImages: 10, + maxPromptLength: 32000, + snapshots: ['gpt-image-1.5-2025-12-16'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-image-1.5', + billing: { + text: { + input: 5, + cachedInput: 1.25, + output: 10, + }, + image: { + input: 8, + cachedInput: 2, + output: 32, + }, + }, + }, + }, + 'chatgpt-image-latest': { + input: ['text', 'image'], + output: ['image'], + providerOptions: {} as GptImage1ProviderOptions, + sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'], + supportsBackground: true, + maxImages: 10, + maxPromptLength: 32000, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/chatgpt-image-latest', + notes: ['Points to the image snapshot currently used in ChatGPT.'], + billing: { + text: { + input: 5, + cachedInput: 1.25, + output: 10, + }, + image: { + input: 8, + cachedInput: 2, + output: 32, + }, + }, + }, + }, + 'gpt-image-1': { + input: ['text', 'image'], + output: ['image'], + providerOptions: {} as GptImage1ProviderOptions, + sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'], + supportsBackground: true, + maxImages: 10, + maxPromptLength: 32000, + lifecycle: { status: 'legacy', replacedBy: 'gpt-image-1.5' }, + }, + 'gpt-image-1-mini': { + input: ['text', 'image'], + output: ['image'], + providerOptions: {} as GptImage1MiniProviderOptions, + sizes: ['1024x1024', '1536x1024', '1024x1536', 'auto'], + supportsBackground: true, + maxImages: 10, + maxPromptLength: 32000, + lifecycle: { status: 'active' }, + }, + 'dall-e-3': { + input: ['text'], + output: ['image'], + providerOptions: {} as DallE3ProviderOptions, + sizes: ['1024x1024', '1792x1024', '1024x1792'], + maxImages: 1, + maxPromptLength: 4000, + lifecycle: { status: 'deprecated', replacedBy: 'gpt-image-1.5' }, + }, + 'dall-e-2': { + input: ['text'], + output: ['image'], + providerOptions: {} as DallE2ProviderOptions, + sizes: ['256x256', '512x512', '1024x1024'], + maxImages: 10, + maxPromptLength: 1000, + lifecycle: { status: 'deprecated', replacedBy: 'gpt-image-1.5' }, + }, +} as const satisfies Record< + string, + ImageModelSpec> +> diff --git a/packages/typescript/ai-openai/src/models/index.ts b/packages/typescript/ai-openai/src/models/index.ts new file mode 100644 index 000000000..31f35edb7 --- /dev/null +++ b/packages/typescript/ai-openai/src/models/index.ts @@ -0,0 +1,4 @@ +export { IMAGE_MODELS } from './image' +export { REALTIME_MODELS, TRANSCRIPTION_MODELS, TTS_MODELS } from './audio' +export { TEXT_MODELS } from './text' +export { VIDEO_MODELS } from './video' diff --git a/packages/typescript/ai-openai/src/models/shared.ts b/packages/typescript/ai-openai/src/models/shared.ts new file mode 100644 index 000000000..bacf74b96 --- /dev/null +++ b/packages/typescript/ai-openai/src/models/shared.ts @@ -0,0 +1,200 @@ +export type OpenAIRegistryStatus = + | 'active' + | 'preview' + | 'deprecated' + | 'legacy' + | 'chatgpt_only' + +export type OpenAIRegistryInput = 'text' | 'image' | 'audio' | 'video' +export type OpenAIRegistryOutput = 'text' | 'image' | 'audio' | 'video' + +export type OpenAIRegistryTool = + | 'web_search' + | 'file_search' + | 'image_generation' + | 'code_interpreter' + | 'mcp' + | 'computer_use' + | 'shell' + | 'local_shell' + | 'apply_patch' + | 'hosted_shell' + | 'skills' + | 'tool_search' + | 'custom' + +export interface OpenAIRegistryLifecycle { + status: OpenAIRegistryStatus + replacedBy?: string + deprecatedAt?: string + sunsetAt?: string +} + +export interface OpenAIRegistryLimits { + contextWindow?: number + maxOutputTokens?: number + knowledgeCutoff?: string +} + +export interface OpenAIRegistryBilling { + input?: number + cachedInput?: number + output?: number + text?: { + input?: number + cachedInput?: number + output?: number + } + image?: { + input?: number + cachedInput?: number + output?: number + } + audio?: { + input?: number + cachedInput?: number + output?: number + } + video?: { + input?: number + cachedInput?: number + output?: number + } + notes?: ReadonlyArray +} + +export interface OpenAIRegistryDocs { + source?: string + limits?: OpenAIRegistryLimits + tools?: ReadonlyArray + billing?: OpenAIRegistryBilling + apiEndpoints?: ReadonlyArray + notes?: ReadonlyArray +} + +export type RegistryId> = Extract< + keyof TRegistry, + string +> + +export type RegistrySnapshotId> = { + [TModel in RegistryId]: TRegistry[TModel] extends { + snapshots: ReadonlyArray + } + ? TSnapshot + : never +}[RegistryId] + +export type RegistryModelId> = + | RegistryId + | RegistrySnapshotId + +type RegistryEntryForModel< + TRegistry extends Record, + TModel extends RegistryModelId, +> = TModel extends RegistryId + ? TRegistry[TModel] + : { + [TBase in RegistryId]: TRegistry[TBase] extends { + snapshots: ReadonlyArray + } + ? TModel extends TSnapshot + ? TRegistry[TBase] + : never + : never + }[RegistryId] + +export type RegistryPropertyByName< + TRegistry extends Record, + TKey extends keyof RegistryEntryForModel>, +> = { + [TModel in RegistryModelId]: RegistryEntryForModel< + TRegistry, + TModel + >[TKey] +} + +export type RegistryEntryByModel< + TRegistry extends Record, + TModel extends RegistryModelId, +> = RegistryEntryForModel + +export type RegistryInputByName< + TRegistry extends Record }>, +> = { + [TModel in RegistryModelId]: RegistryEntryForModel< + TRegistry, + TModel + >['input'] +} + +export type RegistrySizeByName< + TRegistry extends Record }>, +> = { + [TModel in RegistryModelId]: RegistryEntryForModel< + TRegistry, + TModel + >['sizes'][number] +} + +function getSnapshots(meta: unknown): Array { + if ( + typeof meta === 'object' && + meta !== null && + 'snapshots' in meta && + Array.isArray((meta as { snapshots?: Array }).snapshots) + ) { + return (meta as { snapshots?: Array }).snapshots ?? [] + } + return [] +} + +/** + * Returns every supported model identifier in the registry, including snapshots. + */ +export function supportedIds>( + registry: TRegistry, +): ReadonlyArray> { + return Object.entries(registry).flatMap(([id, meta]) => [ + id, + ...getSnapshots(meta), + ]) as unknown as ReadonlyArray> +} + +/** + * Returns only snapshot identifiers from the registry. + */ +export function snapshotIds>( + registry: TRegistry, +): ReadonlyArray> { + return Object.values(registry).flatMap( + getSnapshots, + ) as unknown as ReadonlyArray> +} + +/** + * Returns registry ids whose lifecycle matches the requested status. + */ +export function idsByStatus< + const TRegistry extends Record, + TStatus extends OpenAIRegistryStatus, +>( + registry: TRegistry, + status: TStatus, +): ReadonlyArray< + { + [TModel in RegistryId]: TRegistry[TModel]['lifecycle']['status'] extends TStatus + ? TModel + : never + }[RegistryId] +> { + return Object.entries(registry) + .filter(([, meta]) => meta.lifecycle.status === status) + .map(([id]) => id) as unknown as ReadonlyArray< + { + [TModel in RegistryId]: TRegistry[TModel]['lifecycle']['status'] extends TStatus + ? TModel + : never + }[RegistryId] + > +} diff --git a/packages/typescript/ai-openai/src/models/text.ts b/packages/typescript/ai-openai/src/models/text.ts new file mode 100644 index 000000000..5300e58e4 --- /dev/null +++ b/packages/typescript/ai-openai/src/models/text.ts @@ -0,0 +1,822 @@ +import type { + OpenAIBaseOptions, + OpenAIMetadataOptions, + OpenAIReasoningEffort, + OpenAIReasoningOptions, + OpenAIReasoningSummary, + OpenAIReasoningSummaryWithConcise, + OpenAIStreamingOptions, + OpenAIStructuredOutputOptions, + OpenAIToolsOptions, +} from '../text/text-provider-options' +import type { + OpenAIRegistryDocs, + OpenAIRegistryInput, + OpenAIRegistryOutput, +} from './shared' + +type BaseOptions = OpenAIBaseOptions & OpenAIMetadataOptions + +type UnionToIntersection = + (T extends unknown ? (value: T) => void : never) extends ( + value: infer TIntersection, + ) => void + ? TIntersection + : never + +export interface TextProviderFeatureMap { + base: BaseOptions + reasoning: OpenAIReasoningOptions + reasoningConcise: OpenAIReasoningOptions< + OpenAIReasoningEffort, + OpenAIReasoningSummaryWithConcise + > + structured: OpenAIStructuredOutputOptions + tools: OpenAIToolsOptions + streaming: OpenAIStreamingOptions +} + +type TextProviderFeature = keyof TextProviderFeatureMap +type TextProviderNonReasoningFeature = Exclude< + TextProviderFeature, + 'reasoning' | 'reasoningConcise' +> +type TextProviderNonBaseFeature = Exclude +type NonReasoningTextProviderFeatures = readonly [ + 'base', + ...ReadonlyArray, +] +type ReasoningTextProviderFeatures = readonly [ + 'base', + 'reasoning', + ...ReadonlyArray, +] +type ReasoningConciseTextProviderFeatures = readonly [ + 'base', + 'reasoningConcise', + ...ReadonlyArray, +] + +interface TextReasoningSpec< + TSummary extends OpenAIReasoningSummaryWithConcise = OpenAIReasoningSummaryWithConcise, +> { + efforts: ReadonlyArray + summaries: ReadonlyArray +} + +interface TextModelSpecBase< + TFeatures extends + | NonReasoningTextProviderFeatures + | ReasoningTextProviderFeatures + | ReasoningConciseTextProviderFeatures, + TInput extends ReadonlyArray, + TOutput extends ReadonlyArray, +> { + input: TInput + output: TOutput + features: TFeatures + reasoning?: TextReasoningSpec + lifecycle: { + status: 'active' | 'preview' | 'deprecated' | 'legacy' | 'chatgpt_only' + replacedBy?: string + } + snapshots?: ReadonlyArray + docs?: OpenAIRegistryDocs +} + +type NonReasoningTextModelSpec< + TFeatures extends NonReasoningTextProviderFeatures, + TInput extends ReadonlyArray, + TOutput extends ReadonlyArray, +> = TextModelSpecBase & { + reasoning?: never +} + +type ReasoningTextModelSpec< + TFeatures extends ReasoningTextProviderFeatures, + TInput extends ReadonlyArray, + TOutput extends ReadonlyArray, +> = TextModelSpecBase & { + reasoning: TextReasoningSpec +} + +type ReasoningConciseTextModelSpec< + TFeatures extends ReasoningConciseTextProviderFeatures, + TInput extends ReadonlyArray, + TOutput extends ReadonlyArray, +> = TextModelSpecBase & { + reasoning: TextReasoningSpec +} + +type TextModelSpec< + TFeatures extends + | NonReasoningTextProviderFeatures + | ReasoningTextProviderFeatures + | ReasoningConciseTextProviderFeatures, + TInput extends ReadonlyArray, + TOutput extends ReadonlyArray, +> = + | NonReasoningTextModelSpec< + Extract, + TInput, + TOutput + > + | ReasoningTextModelSpec< + Extract, + TInput, + TOutput + > + | ReasoningConciseTextModelSpec< + Extract, + TInput, + TOutput + > + +const COMMON_TOOLS = [ + 'web_search', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', +] as const + +const CODING_TOOLS = [ + ...COMMON_TOOLS, + 'shell', + 'local_shell', + 'apply_patch', + 'hosted_shell', + 'skills', + 'tool_search', + 'custom', +] as const + +const DEFAULT_REASONING_SUMMARIES = ['auto', 'detailed'] as const satisfies + ReadonlyArray +const CONCISE_REASONING_SUMMARIES = [ + 'auto', + 'detailed', + 'concise', +] as const satisfies ReadonlyArray + +type TextReasoningOptionsForEntry = TEntry extends { + reasoning: { + efforts: ReadonlyArray + summaries: ReadonlyArray< + infer TSummary extends OpenAIReasoningSummaryWithConcise + > + } +} + ? OpenAIReasoningOptions + : {} + +export type TextProviderOptionsForEntry< + TEntry extends { features: ReadonlyArray }, +> = + UnionToIntersection< + TextProviderFeatureMap[ + Extract + ] + > & + TextReasoningOptionsForEntry + +export const TEXT_MODELS = { + 'gpt-5.4': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.4-2026-03-05'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.4', + limits: { + contextWindow: 1_050_000, + maxOutputTokens: 128_000, + knowledgeCutoff: '2025-08-31', + }, + tools: CODING_TOOLS, + billing: { + input: 2.5, + cachedInput: 0.25, + output: 15, + notes: [ + 'Prompts over 272K input tokens increase pricing for the full session.', + 'Regional processing endpoints apply a 10% uplift.', + ], + }, + }, + }, + 'gpt-5.4-pro': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.4-pro-2026-03-05'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.4-pro', + limits: { + contextWindow: 1_050_000, + maxOutputTokens: 128_000, + knowledgeCutoff: '2025-08-31', + }, + tools: COMMON_TOOLS, + billing: { + input: 30, + output: 180, + notes: [ + 'Responses-only model.', + 'Prompts over 272K input tokens increase pricing for the full session.', + ], + }, + }, + }, + 'gpt-5.4-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.4-mini-2026-03-17'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.4-mini', + tools: CODING_TOOLS, + }, + }, + 'gpt-5.4-nano': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.4-nano-2026-03-17'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.4-nano', + tools: COMMON_TOOLS, + }, + }, + 'gpt-5.3-chat-latest': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.3-chat-latest', + limits: { + contextWindow: 128_000, + maxOutputTokens: 16_384, + knowledgeCutoff: '2025-08-31', + }, + tools: COMMON_TOOLS, + billing: { + input: 1.75, + cachedInput: 0.175, + output: 14, + }, + }, + }, + 'gpt-5.3-codex': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.3-codex', + limits: { + contextWindow: 400_000, + maxOutputTokens: 128_000, + knowledgeCutoff: '2025-08-31', + }, + tools: CODING_TOOLS, + billing: { + input: 1.75, + cachedInput: 0.175, + output: 14, + }, + }, + }, + 'gpt-5.2-codex': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.2-codex', + limits: { + contextWindow: 400_000, + maxOutputTokens: 128_000, + knowledgeCutoff: '2025-08-31', + }, + tools: CODING_TOOLS, + billing: { + input: 1.75, + cachedInput: 0.175, + output: 14, + }, + }, + }, + 'gpt-5.1-codex-max': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.1-codex-max', + limits: { + contextWindow: 400_000, + maxOutputTokens: 128_000, + knowledgeCutoff: '2024-09-30', + }, + tools: CODING_TOOLS, + billing: { + input: 1.25, + cachedInput: 0.125, + output: 10, + }, + }, + }, + 'gpt-5.2': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.2-2025-12-11'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4' }, + }, + 'gpt-5.2-pro': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['medium', 'high', 'xhigh'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.2-pro-2025-12-11'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4-pro' }, + }, + 'gpt-5.2-chat-latest': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.3-chat-latest' }, + }, + 'gpt-5.1': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5.1-2025-11-13'], + lifecycle: { status: 'active' }, + }, + 'gpt-5.1-codex': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.1-codex', + tools: CODING_TOOLS, + notes: ['Codex models are text-output only.'], + }, + }, + 'gpt-5.1-codex-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['none', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5.1-codex-mini', + tools: CODING_TOOLS, + notes: ['Codex models are text-output only.'], + }, + }, + 'gpt-5.1-chat-latest': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.3-chat-latest' }, + }, + 'gpt-5': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['minimal', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5-2025-08-07'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4' }, + }, + 'gpt-5-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['minimal', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5-mini-2025-08-07'], + lifecycle: { status: 'active' }, + }, + 'gpt-5-nano': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['minimal', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5-nano-2025-08-07'], + lifecycle: { status: 'active' }, + }, + 'gpt-5-pro': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['gpt-5-pro-2025-10-06'], + lifecycle: { status: 'active' }, + }, + 'gpt-5-codex': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['minimal', 'low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-5-codex', + tools: CODING_TOOLS, + notes: ['Codex models are text-output only.'], + }, + }, + 'gpt-5-chat-latest': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.3-chat-latest' }, + }, + 'gpt-4.5-preview': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4.5-preview-2025-02-27'], + lifecycle: { status: 'deprecated', replacedBy: 'gpt-5.4' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4.5-preview', + }, + }, + 'gpt-4.1': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4.1-2025-04-14'], + lifecycle: { status: 'active' }, + }, + 'gpt-4.1-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4.1-mini-2025-04-14'], + lifecycle: { status: 'active' }, + }, + 'gpt-4.1-nano': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4.1-nano-2025-04-14'], + lifecycle: { status: 'active' }, + }, + 'gpt-4o': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4o-2024-11-20', 'gpt-4o-2024-08-06', 'gpt-4o-2024-05-13'], + lifecycle: { status: 'active' }, + }, + 'gpt-4o-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'structured', 'tools', 'streaming'] as const, + snapshots: ['gpt-4o-mini-2024-07-18'], + lifecycle: { status: 'active' }, + }, + 'gpt-4-turbo': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'tools', 'streaming'] as const, + lifecycle: { status: 'legacy', replacedBy: 'gpt-4.1' }, + snapshots: ['gpt-4-turbo-2024-04-09'], + }, + 'gpt-4': { + input: ['text'], + output: ['text'], + features: ['base', 'streaming'] as const, + snapshots: ['gpt-4-0613', 'gpt-4-0314'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-4.1' }, + }, + 'gpt-3.5-turbo': { + input: ['text'], + output: ['text'], + features: ['base'] as const, + snapshots: ['gpt-3.5-turbo-0125', 'gpt-3.5-turbo-1106'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-4o-mini' }, + }, + 'gpt-4o-search-preview': { + input: ['text'], + output: ['text'], + features: ['base', 'structured', 'streaming'] as const, + snapshots: ['gpt-4o-search-preview-2025-03-11'], + lifecycle: { status: 'preview' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-search-preview', + tools: ['web_search'], + }, + }, + 'gpt-4o-mini-search-preview': { + input: ['text'], + output: ['text'], + features: ['base', 'structured', 'streaming'] as const, + snapshots: ['gpt-4o-mini-search-preview-2025-03-11'], + lifecycle: { status: 'preview' }, + docs: { + source: + 'https://developers.openai.com/api/docs/models/gpt-4o-mini-search-preview', + tools: ['web_search'], + }, + }, + 'computer-use-preview': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoningConcise', 'tools'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: CONCISE_REASONING_SUMMARIES, + }, + snapshots: ['computer-use-preview-2025-03-11'], + lifecycle: { status: 'preview' }, + docs: { + tools: ['computer_use', 'hosted_shell', 'apply_patch', 'tool_search'], + source: 'https://developers.openai.com/api/docs/models/computer-use-preview', + }, + }, + 'gpt-audio-1.5': { + input: ['text', 'audio'], + output: ['text', 'audio'], + features: ['base', 'tools'] as const, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-audio-1.5', + tools: COMMON_TOOLS, + limits: { + contextWindow: 32_768, + maxOutputTokens: 4_096, + knowledgeCutoff: '2024-06-01', + }, + billing: { + audio: { + input: 4, + cachedInput: 0.4, + output: 16, + }, + }, + }, + }, + 'gpt-audio': { + input: ['text', 'audio'], + output: ['text', 'audio'], + features: ['base', 'tools'] as const, + snapshots: ['gpt-audio-2025-08-28'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-audio', + tools: COMMON_TOOLS, + }, + }, + 'gpt-audio-mini': { + input: ['text', 'audio'], + output: ['text', 'audio'], + features: ['base', 'tools'] as const, + snapshots: ['gpt-audio-mini-2025-12-15', 'gpt-audio-mini-2025-10-06'], + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-audio-mini', + tools: COMMON_TOOLS, + }, + }, + 'gpt-4o-audio-preview': { + input: ['text', 'audio'], + output: ['text', 'audio'], + features: ['base', 'tools', 'streaming'] as const, + snapshots: [ + 'gpt-4o-audio-preview-2025-06-03', + 'gpt-4o-audio-preview-2024-12-17', + 'gpt-4o-audio-preview-2024-10-01', + ], + lifecycle: { status: 'preview' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-audio-preview', + tools: COMMON_TOOLS, + }, + }, + 'gpt-4o-mini-audio-preview': { + input: ['text', 'audio'], + output: ['text', 'audio'], + features: ['base', 'tools', 'streaming'] as const, + snapshots: ['gpt-4o-mini-audio-preview-2024-12-17'], + lifecycle: { status: 'preview' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-4o-mini-audio-preview', + tools: COMMON_TOOLS, + }, + }, + 'gpt-oss-120b': { + input: ['text'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-oss-120b', + }, + }, + 'gpt-oss-20b': { + input: ['text'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/gpt-oss-20b', + }, + }, + 'o4-mini': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o4-mini-2025-04-16'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4-mini' }, + }, + 'o4-mini-deep-research': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o4-mini-deep-research-2025-06-26'], + lifecycle: { status: 'active' }, + }, + o3: { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o3-2025-04-16'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4' }, + }, + 'o3-pro': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o3-pro-2025-06-10'], + lifecycle: { status: 'active' }, + }, + 'o3-mini': { + input: ['text'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o3-mini-2025-01-31'], + lifecycle: { status: 'active' }, + }, + 'o3-deep-research': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o3-deep-research-2025-06-26'], + lifecycle: { status: 'active' }, + }, + 'o1-preview': { + input: ['text'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o1-preview-2024-09-12'], + lifecycle: { status: 'deprecated', replacedBy: 'o1' }, + }, + 'o1-mini': { + input: ['text'], + output: ['text'], + features: ['base', 'reasoning', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o1-mini-2024-09-12'], + lifecycle: { status: 'deprecated', replacedBy: 'o4-mini' }, + }, + o1: { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools', 'streaming'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o1-2024-12-17'], + lifecycle: { status: 'legacy', replacedBy: 'gpt-5.4' }, + }, + 'o1-pro': { + input: ['text', 'image'], + output: ['text'], + features: ['base', 'reasoning', 'structured', 'tools'] as const, + reasoning: { + efforts: ['low', 'medium', 'high'] as const, + summaries: DEFAULT_REASONING_SUMMARIES, + }, + snapshots: ['o1-pro-2025-03-19'], + lifecycle: { status: 'active' }, + }, +} as const satisfies Record< + string, + TextModelSpec< + | NonReasoningTextProviderFeatures + | ReasoningTextProviderFeatures + | ReasoningConciseTextProviderFeatures, + ReadonlyArray, + ReadonlyArray + > +> diff --git a/packages/typescript/ai-openai/src/models/video.ts b/packages/typescript/ai-openai/src/models/video.ts new file mode 100644 index 000000000..1f382e8f2 --- /dev/null +++ b/packages/typescript/ai-openai/src/models/video.ts @@ -0,0 +1,55 @@ +import type { + OpenAIVideoProviderOptions, + OpenAIVideoSeconds, + OpenAIVideoSize, +} from '../video/video-provider-options' +import type { OpenAIRegistryDocs } from './shared' + +interface VideoModelSpec { + input: readonly ['text', 'image'] + output: readonly ['video', 'audio'] + providerOptions: OpenAIVideoProviderOptions + sizes: ReadonlyArray + durations: ReadonlyArray + snapshots?: ReadonlyArray + lifecycle: { + status: 'active' + } + docs?: OpenAIRegistryDocs +} + +const VIDEO_SIZES = [ + '1280x720', + '720x1280', + '1792x1024', + '1024x1792', +] as const satisfies ReadonlyArray + +const VIDEO_DURATIONS = ['4', '8', '12'] as const satisfies ReadonlyArray + +export const VIDEO_MODELS = { + 'sora-2': { + input: ['text', 'image'], + output: ['video', 'audio'], + providerOptions: {}, + sizes: VIDEO_SIZES, + durations: VIDEO_DURATIONS, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/sora-2', + billing: { output: 0.1 }, + }, + }, + 'sora-2-pro': { + input: ['text', 'image'], + output: ['video', 'audio'], + providerOptions: {}, + sizes: VIDEO_SIZES, + durations: VIDEO_DURATIONS, + lifecycle: { status: 'active' }, + docs: { + source: 'https://developers.openai.com/api/docs/models/sora-2-pro', + billing: { output: 0.5 }, + }, + }, +} as const satisfies Record diff --git a/packages/typescript/ai-openai/src/realtime/adapter.ts b/packages/typescript/ai-openai/src/realtime/adapter.ts index 35187a5d2..7cfaa08ed 100644 --- a/packages/typescript/ai-openai/src/realtime/adapter.ts +++ b/packages/typescript/ai-openai/src/realtime/adapter.ts @@ -13,6 +13,7 @@ import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client' import type { OpenAIRealtimeOptions } from './types' const OPENAI_REALTIME_URL = 'https://api.openai.com/v1/realtime' +type SessionToolConfig = NonNullable[number] /** * Creates an OpenAI realtime adapter for client-side use. @@ -59,7 +60,7 @@ export function openaiRealtime( async function createWebRTCConnection( token: RealtimeToken, ): Promise { - const model = token.config.model ?? 'gpt-4o-realtime-preview' + const model = token.config.model ?? 'gpt-realtime-1.5' const eventHandlers = new Map>>() // WebRTC peer connection @@ -548,7 +549,7 @@ async function createWebRTCConnection( } if (config.tools !== undefined) { - sessionUpdate.tools = config.tools.map((t) => ({ + sessionUpdate.tools = config.tools.map((t: SessionToolConfig) => ({ type: 'function', name: t.name, description: t.description, diff --git a/packages/typescript/ai-openai/src/realtime/token.ts b/packages/typescript/ai-openai/src/realtime/token.ts index 6bff9c9c2..172481203 100644 --- a/packages/typescript/ai-openai/src/realtime/token.ts +++ b/packages/typescript/ai-openai/src/realtime/token.ts @@ -25,7 +25,7 @@ const OPENAI_REALTIME_SESSIONS_URL = * * const token = await realtimeToken({ * adapter: openaiRealtimeToken({ - * model: 'gpt-4o-realtime-preview', + * model: 'gpt-realtime-1.5', * voice: 'alloy', * instructions: 'You are a helpful assistant.', * turnDetection: { @@ -46,7 +46,7 @@ export function openaiRealtimeToken( async generateToken(): Promise { const model: OpenAIRealtimeModel = - options.model ?? 'gpt-4o-realtime-preview' + options.model ?? 'gpt-realtime-1.5' // Call OpenAI API to create session and get ephemeral token. // Only the model is sent server-side; all other session config diff --git a/packages/typescript/ai-openai/src/realtime/types.ts b/packages/typescript/ai-openai/src/realtime/types.ts index f4d36d9cc..a02d89e35 100644 --- a/packages/typescript/ai-openai/src/realtime/types.ts +++ b/packages/typescript/ai-openai/src/realtime/types.ts @@ -1,4 +1,5 @@ import type { VADConfig } from '@tanstack/ai' +import type { OpenAIRealtimeModel as CatalogRealtimeModel } from '../meta/realtime' /** * OpenAI realtime voice options @@ -18,13 +19,7 @@ export type OpenAIRealtimeVoice = /** * OpenAI realtime model options */ -export type OpenAIRealtimeModel = - | 'gpt-4o-realtime-preview' - | 'gpt-4o-realtime-preview-2024-10-01' - | 'gpt-4o-mini-realtime-preview' - | 'gpt-4o-mini-realtime-preview-2024-12-17' - | 'gpt-realtime' - | 'gpt-realtime-mini' +export type OpenAIRealtimeModel = CatalogRealtimeModel /** * OpenAI semantic VAD configuration @@ -54,7 +49,7 @@ export type OpenAITurnDetection = * Options for the OpenAI realtime token adapter */ export interface OpenAIRealtimeTokenOptions { - /** Model to use (default: 'gpt-4o-realtime-preview') */ + /** Model to use (default: 'gpt-realtime-1.5') */ model?: OpenAIRealtimeModel } diff --git a/packages/typescript/ai-openai/src/text/text-provider-options.ts b/packages/typescript/ai-openai/src/text/text-provider-options.ts index ba9d60498..993928d0f 100644 --- a/packages/typescript/ai-openai/src/text/text-provider-options.ts +++ b/packages/typescript/ai-openai/src/text/text-provider-options.ts @@ -126,14 +126,26 @@ https://platform.openai.com/docs/api-reference/responses/create#responses_create // Feature fragments that can be stitched per-model -// Shared base types for reasoning options -type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' -type ReasoningSummary = 'auto' | 'detailed' +export type OpenAIReasoningEffort = + | 'none' + | 'minimal' + | 'low' + | 'medium' + | 'high' + | 'xhigh' +export type OpenAIReasoningSummary = 'auto' | 'detailed' +export type OpenAIReasoningSummaryWithConcise = + | OpenAIReasoningSummary + | 'concise' /** - * Reasoning options for most models (excludes 'concise' summary). + * Generic reasoning options fragment. Exact model unions should be derived from + * the model registry, not from this broad helper alone. */ -export interface OpenAIReasoningOptions { +export type OpenAIReasoningOptions< + TEffort extends OpenAIReasoningEffort = OpenAIReasoningEffort, + TSummary extends OpenAIReasoningSummaryWithConcise = OpenAIReasoningSummary, +> = { /** * Reasoning controls for models that support it. * Lets you guide how much chain-of-thought computation to spend. @@ -146,40 +158,21 @@ export interface OpenAIReasoningOptions { * All models before gpt-5.1 default to medium reasoning effort, and do not support none. * The gpt-5-pro model defaults to (and only supports) high reasoning effort. */ - effort?: ReasoningEffort + effort?: TEffort /** * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. * https://platform.openai.com/docs/api-reference/responses/create#responses_create-reasoning-summary */ - summary?: ReasoningSummary + summary?: TSummary } } /** - * Reasoning options for computer-use-preview model (includes 'concise' summary). + * Backwards-compatible alias for the variant that can include `concise`. */ -export interface OpenAIReasoningOptionsWithConcise { - /** - * Reasoning controls for models that support it. - * Lets you guide how much chain-of-thought computation to spend. - * https://platform.openai.com/docs/api-reference/responses/create#responses_create-reasoning - * https://platform.openai.com/docs/guides/reasoning - */ - reasoning?: { - /** - * gpt-5.1 defaults to none, which does not perform reasoning. The supported reasoning values for gpt-5.1 are none, low, medium, and high. Tool calls are supported for all reasoning values in gpt-5.1. - * All models before gpt-5.1 default to medium reasoning effort, and do not support none. - * The gpt-5-pro model defaults to (and only supports) high reasoning effort. - */ - effort?: ReasoningEffort - /** - * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. - * `concise` is only supported for `computer-use-preview` models. - * https://platform.openai.com/docs/api-reference/responses/create#responses_create-reasoning-summary - */ - summary?: ReasoningSummary | 'concise' - } -} +export type OpenAIReasoningOptionsWithConcise< + TEffort extends OpenAIReasoningEffort = OpenAIReasoningEffort, +> = OpenAIReasoningOptions export interface OpenAIStructuredOutputOptions { /** @@ -234,8 +227,10 @@ https://platform.openai.com/docs/api-reference/responses/create#responses_create metadata?: Record } +type AnyReasoningOptions = OpenAIReasoningOptionsWithConcise + export type ExternalTextProviderOptions = OpenAIBaseOptions & - OpenAIReasoningOptions & + AnyReasoningOptions & OpenAIStructuredOutputOptions & OpenAIToolsOptions & OpenAIStreamingOptions & @@ -317,6 +312,9 @@ const validateConversationAndPreviousResponseId = ( } } +/** + * Runs the shared validation rules for OpenAI text provider options. + */ export const validateTextProviderOptions = ( options: InternalTextProviderOptions, ) => { diff --git a/packages/typescript/ai-openai/src/video/video-provider-options.ts b/packages/typescript/ai-openai/src/video/video-provider-options.ts index b0f337039..4d11c7ac5 100644 --- a/packages/typescript/ai-openai/src/video/video-provider-options.ts +++ b/packages/typescript/ai-openai/src/video/video-provider-options.ts @@ -1,3 +1,5 @@ +import { VIDEO_MODELS } from '../models/video' + /** * OpenAI Video Generation Provider Options * @@ -46,26 +48,6 @@ export interface OpenAIVideoProviderOptions { seconds?: OpenAIVideoSeconds } -/** - * Model-specific provider options mapping. - * - * @experimental Video generation is an experimental feature and may change. - */ -export type OpenAIVideoModelProviderOptionsByName = { - 'sora-2': OpenAIVideoProviderOptions - 'sora-2-pro': OpenAIVideoProviderOptions -} - -/** - * Model-specific provider options mapping. - * - * @experimental Video generation is an experimental feature and may change. - */ -export type OpenAIVideoModelSizeByName = { - 'sora-2': OpenAIVideoSize - 'sora-2-pro': OpenAIVideoSize -} - /** * Validate video size for a given model. * @@ -75,12 +57,11 @@ export function validateVideoSize( model: string, size?: string, ): asserts size is OpenAIVideoSize | undefined { - const validSizes: Array = [ - '1280x720', - '720x1280', - '1792x1024', - '1024x1792', - ] + if (!Object.hasOwn(VIDEO_MODELS, model)) { + throw new Error(`Unknown video model: ${model}`) + } + + const validSizes = VIDEO_MODELS[model as keyof typeof VIDEO_MODELS].sizes if (size && !validSizes.includes(size as OpenAIVideoSize)) { throw new Error( @@ -99,14 +80,17 @@ export function validateVideoSeconds( model: string, seconds?: number | string, ): asserts seconds is OpenAIVideoSeconds | number | undefined { - const validSeconds: Array = ['4', '8', '12'] - const validNumbers: Array = [4, 8, 12] + if (!Object.hasOwn(VIDEO_MODELS, model)) { + throw new Error(`Unknown video model: ${model}`) + } + + const validSeconds = VIDEO_MODELS[model as keyof typeof VIDEO_MODELS].durations if (seconds !== undefined) { const isValid = typeof seconds === 'string' - ? validSeconds.includes(seconds) - : validNumbers.includes(seconds) + ? validSeconds.includes(seconds as OpenAIVideoSeconds) + : validSeconds.map(Number).includes(seconds) if (!isValid) { throw new Error( diff --git a/packages/typescript/ai-openai/tests/audio-provider-options.test.ts b/packages/typescript/ai-openai/tests/audio-provider-options.test.ts new file mode 100644 index 000000000..f88b42d02 --- /dev/null +++ b/packages/typescript/ai-openai/tests/audio-provider-options.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + validateInstructions, + validateStreamFormat, +} from '../src/audio/audio-provider-options' + +describe('audio provider option validation', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('validateStreamFormat', () => { + it('warns when stream_format is used with an unknown model', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + expect(() => + validateStreamFormat({ + input: 'hello', + model: 'not-a-real-model', + stream_format: 'audio', + }), + ).not.toThrow() + + expect(warnSpy).toHaveBeenCalledWith( + 'Unknown TTS model: not-a-real-model. stream_format may not be supported.', + ) + }) + + it('throws when streaming is not supported by a known model', () => { + expect(() => + validateStreamFormat({ + input: 'hello', + model: 'tts-1', + stream_format: 'sse', + }), + ).toThrow('The model tts-1 does not support streaming.') + }) + }) + + describe('validateInstructions', () => { + it('throws for an unknown model', () => { + expect(() => + validateInstructions({ + input: 'hello', + model: 'not-a-real-model', + instructions: 'speak calmly', + }), + ).toThrow('Unknown TTS model: not-a-real-model') + }) + + it('throws when instructions are not supported by a known model', () => { + expect(() => + validateInstructions({ + input: 'hello', + model: 'tts-1', + instructions: 'speak calmly', + }), + ).toThrow('The model tts-1 does not support instructions.') + }) + }) +}) diff --git a/packages/typescript/ai-openai/tests/image-adapter.test.ts b/packages/typescript/ai-openai/tests/image-adapter.test.ts index 49d3353c0..680a261bb 100644 --- a/packages/typescript/ai-openai/tests/image-adapter.test.ts +++ b/packages/typescript/ai-openai/tests/image-adapter.test.ts @@ -127,6 +127,12 @@ describe('OpenAI Image Adapter', () => { validatePrompt({ prompt: 'A cat', model: 'gpt-image-1' }), ).not.toThrow() }) + + it('throws for an unknown model', () => { + expect(() => + validatePrompt({ prompt: 'A cat', model: 'not-a-real-model' }), + ).toThrow('Unknown image model: not-a-real-model') + }) }) describe('generateImages', () => { @@ -183,6 +189,42 @@ describe('OpenAI Image Adapter', () => { }) }) + it('passes modelOptions through to the OpenAI images.generate API', async () => { + const mockGenerate = vi.fn().mockResolvedValueOnce({ + data: [{ b64_json: 'base64encodedimage' }], + }) + + const adapter = createOpenaiImage('gpt-image-1', 'test-api-key') + ;( + adapter as unknown as { client: { images: { generate: unknown } } } + ).client = { + images: { + generate: mockGenerate, + }, + } + + await adapter.generateImages({ + model: 'gpt-image-1', + prompt: 'A cat wearing a hat', + size: '1024x1024', + modelOptions: { + background: 'transparent', + output_format: 'webp', + }, + }) + + expect(mockGenerate).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-image-1', + prompt: 'A cat wearing a hat', + size: '1024x1024', + background: 'transparent', + output_format: 'webp', + stream: false, + }), + ) + }) + it('generates a unique ID for each response', async () => { const mockResponse = { data: [{ b64_json: 'base64' }], diff --git a/packages/typescript/ai-openai/tests/model-meta.test.ts b/packages/typescript/ai-openai/tests/model-meta.test.ts index 648deaa87..8139bf19d 100644 --- a/packages/typescript/ai-openai/tests/model-meta.test.ts +++ b/packages/typescript/ai-openai/tests/model-meta.test.ts @@ -1,1285 +1,334 @@ -import { describe, it, expectTypeOf } from 'vitest' -import type { - OpenAIChatModelProviderOptionsByName, - OpenAIModelInputModalitiesByName, +import { describe, expect, expectTypeOf, it } from 'vitest' +import { + IMAGE_MODELS, + TEXT_MODELS, + VIDEO_MODELS, +} from '../src/models' +import { + REALTIME_MODELS, + TRANSCRIPTION_MODELS, + TTS_MODELS, +} from '../src/models/audio' +import { + idsByStatus, + snapshotIds, + supportedIds, + type RegistryModelId, +} from '../src/models/shared' +import { + OPENAI_CHAT_MODELS, + OPENAI_CHAT_SNAPSHOT_MODELS, + OPENAI_CURRENT_CHAT_MODELS, + OPENAI_CURRENT_IMAGE_MODELS, + OPENAI_PREVIEW_CHAT_MODELS, + OPENAI_CURRENT_TRANSCRIPTION_MODELS, + OPENAI_CURRENT_TTS_MODELS, + OPENAI_CURRENT_VIDEO_MODELS, + OPENAI_DEPRECATED_CHAT_MODELS, + OPENAI_IMAGE_MODELS, + OPENAI_IMAGE_SNAPSHOT_MODELS, + OPENAI_REALTIME_MODELS, + OPENAI_REALTIME_SNAPSHOT_MODELS, + OPENAI_TRANSCRIPTION_MODELS, + OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS, + OPENAI_TTS_MODELS, + OPENAI_TTS_SNAPSHOT_MODELS, + OPENAI_VIDEO_MODELS, + type OpenAIChatModel, + type OpenAIChatModelProviderOptionsByName, + type OpenAIImageModelProviderOptionsByName, + type OpenAIImageModelSizeByName, + type OpenAIModelInputModalitiesByName, + type OpenAIRealtimeModel, + type OpenAITTSModel, + type OpenAITranscriptionModel, + type OpenAIVideoModel, + type OpenAIVideoModelProviderOptionsByName, + type OpenAIVideoModelSizeByName, } from '../src/model-meta' import type { - OpenAIBaseOptions, - OpenAIReasoningOptions, + DallE3ProviderOptions, + GptImage1ProviderOptions, +} from '../src/image/image-provider-options' +import type { OpenAIReasoningOptionsWithConcise, + OpenAIStreamingOptions, OpenAIStructuredOutputOptions, OpenAIToolsOptions, - OpenAIStreamingOptions, - OpenAIMetadataOptions, } from '../src/text/text-provider-options' -import type { OpenAIMessageMetadataByModality } from '../src/message-types' import type { - AudioPart, - ConstrainedModelMessage, - DocumentPart, - ImagePart, - Modality, - TextPart, - VideoPart, -} from '@tanstack/ai' - -/** - * Helper type to construct InputModalitiesTypes from modalities array and metadata. - * This is used to properly type ConstrainedModelMessage in tests. - */ -type MakeInputModalitiesTypes> = { - inputModalities: TModalities - messageMetadataByModality: OpenAIMessageMetadataByModality -} - -/** - * Type assertion tests for OpenAI model provider options. - * - * These tests verify that: - * 1. Models with reasoning support have OpenAIReasoningOptions in their provider options - * 2. Models without reasoning support do NOT have OpenAIReasoningOptions - * 3. Models with structured output support have OpenAIStructuredOutputOptions - * 4. Models without structured output support do NOT have OpenAIStructuredOutputOptions - * 5. Models with tools support have OpenAIToolsOptions - * 6. All chat models have base options (OpenAIBaseOptions, OpenAIMetadataOptions) - */ - -// Base options that ALL chat models should have -type BaseOptions = OpenAIBaseOptions & OpenAIMetadataOptions - -// Full featured model options (reasoning + structured output + tools + streaming) -type FullFeaturedOptions = BaseOptions & - OpenAIReasoningOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions - -// Standard model options (structured output + tools + streaming, no reasoning) -type StandardOptions = BaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions - -// Reasoning-only model options (reasoning but no tools/structured output streaming) -type ReasoningOnlyOptions = BaseOptions & OpenAIReasoningOptions - -describe('OpenAI Chat Model Provider Options Type Assertions', () => { - describe('Models WITH reasoning AND structured output AND tools support (Full Featured)', () => { - it('gpt-5.1 should support all features', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5.1'] - - // Should have reasoning options - expectTypeOf().toExtend() - - // Should have structured output options - expectTypeOf().toExtend() - - // Should have tools options - expectTypeOf().toExtend() - - // Should have streaming options - expectTypeOf().toExtend() - - // Should have base options - expectTypeOf().toExtend() - - // Verify specific properties exist - expectTypeOf().toHaveProperty('reasoning') - expectTypeOf().toHaveProperty('text') - expectTypeOf().toHaveProperty('tool_choice') - expectTypeOf().toHaveProperty('stream_options') - expectTypeOf().toHaveProperty('metadata') - expectTypeOf().toHaveProperty('store') - }) - - it('gpt-5.1-codex should support all features', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5.1-codex'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-5 should support all features', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-5-pro should support all features', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5-pro'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Models WITH reasoning AND structured output AND tools (gpt-5-mini/nano)', () => { - it('gpt-5-mini should have reasoning, structured output and tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - - // Should have reasoning options - expectTypeOf().toExtend() - - // Should have structured output options - expectTypeOf().toExtend() - - // Should have tools options - expectTypeOf().toExtend() - - // Should have streaming options - expectTypeOf().toExtend() - - // Should have base options - expectTypeOf().toExtend() - }) - - it('gpt-5-nano should have reasoning, structured output and tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5-nano'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Models WITH structured output AND tools but WITHOUT reasoning (Standard)', () => { - it('gpt-5-codex should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5-codex'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4.1 should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4.1'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4.1-mini should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4.1-mini'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4.1-nano should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4.1-nano'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-mini should have structured output and tools but NOT reasoning', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-mini'] - - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Models WITH reasoning but LIMITED other features (Reasoning Models)', () => { - it('o3 should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o3'] - - // Should have reasoning options - expectTypeOf().toExtend() - - // Should NOT have structured output options - expectTypeOf().not.toExtend() - - // Should NOT have tools options - expectTypeOf().not.toExtend() - - // Should NOT have streaming options - expectTypeOf().not.toExtend() - - // Should have base options - expectTypeOf().toExtend() - }) - - it('o3-pro should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o3-pro'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o3-mini should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o3-mini'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o4-mini should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o4-mini'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o3-deep-research should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o3-deep-research'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o4-mini-deep-research should have reasoning but NOT structured output or tools', () => { - type Options = - OpenAIChatModelProviderOptionsByName['o4-mini-deep-research'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o1 should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o1'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('o1-pro should have reasoning but NOT structured output or tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['o1-pro'] - - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Models WITH tools but WITHOUT structured output or reasoning (Legacy Models)', () => { - it('gpt-4 should have tools and streaming but NOT reasoning or structured output', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4-turbo should have tools and streaming but NOT reasoning or structured output', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4-turbo'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-3.5-turbo should have tools and streaming but NOT reasoning or structured output', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Models WITH minimal features (Basic Models)', () => { - it('chatgpt-4.0 should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-audio should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-audio'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-audio-mini should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-audio-mini'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-audio should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-audio'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-mini-audio should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-mini-audio'] - - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Chat-only models WITH reasoning AND structured output but WITHOUT tools', () => { - it('gpt-5.1-chat should have reasoning and structured output but NOT tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5.1-chat'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-5-chat should have reasoning and structured output but NOT tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5-chat'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Codex/Preview models', () => { - it('gpt-5.1-codex-mini should have structured output and tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5.1-codex-mini'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('codex-mini-latest should have structured output and tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['codex-mini-latest'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-search-preview should have structured output and tools', () => { - type Options = - OpenAIChatModelProviderOptionsByName['gpt-4o-search-preview'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-mini-search-preview should have structured output and tools', () => { - type Options = - OpenAIChatModelProviderOptionsByName['gpt-4o-mini-search-preview'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('computer-use-preview should have tools and reasoning with concise but NOT structured output', () => { - type Options = - OpenAIChatModelProviderOptionsByName['computer-use-preview'] - - // Should have reasoning options with 'concise' summary support - expectTypeOf().toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - }) - - describe('Provider options type completeness', () => { - it('OpenAIChatModelProviderOptionsByName should have entries for all chat models', () => { - type Keys = keyof OpenAIChatModelProviderOptionsByName - - // Full featured models - expectTypeOf<'gpt-5.1'>().toExtend() - expectTypeOf<'gpt-5.1-codex'>().toExtend() - expectTypeOf<'gpt-5'>().toExtend() - expectTypeOf<'gpt-5-pro'>().toExtend() - - // Standard models (structured output + tools, no reasoning) - expectTypeOf<'gpt-5-mini'>().toExtend() - expectTypeOf<'gpt-5-nano'>().toExtend() - expectTypeOf<'gpt-5-codex'>().toExtend() - expectTypeOf<'gpt-4.1'>().toExtend() - expectTypeOf<'gpt-4.1-mini'>().toExtend() - expectTypeOf<'gpt-4.1-nano'>().toExtend() - expectTypeOf<'gpt-4o'>().toExtend() - expectTypeOf<'gpt-4o-mini'>().toExtend() - - // Reasoning-only models - expectTypeOf<'o3'>().toExtend() - expectTypeOf<'o3-pro'>().toExtend() - expectTypeOf<'o3-mini'>().toExtend() - expectTypeOf<'o4-mini'>().toExtend() - expectTypeOf<'o3-deep-research'>().toExtend() - expectTypeOf<'o4-mini-deep-research'>().toExtend() - expectTypeOf<'o1'>().toExtend() - expectTypeOf<'o1-pro'>().toExtend() - - // Legacy models - expectTypeOf<'gpt-4'>().toExtend() - expectTypeOf<'gpt-4-turbo'>().toExtend() - expectTypeOf<'gpt-3.5-turbo'>().toExtend() - - // Basic models - expectTypeOf<'chatgpt-4.0'>().toExtend() - expectTypeOf<'gpt-audio'>().toExtend() - expectTypeOf<'gpt-audio-mini'>().toExtend() - expectTypeOf<'gpt-4o-audio'>().toExtend() - expectTypeOf<'gpt-4o-mini-audio'>().toExtend() - - // Chat-only models - expectTypeOf<'gpt-5.1-chat'>().toExtend() - expectTypeOf<'gpt-5-chat'>().toExtend() - - // Codex/Preview models - expectTypeOf<'gpt-5.1-codex-mini'>().toExtend() - expectTypeOf<'codex-mini-latest'>().toExtend() - expectTypeOf<'gpt-4o-search-preview'>().toExtend() - expectTypeOf<'gpt-4o-mini-search-preview'>().toExtend() - expectTypeOf<'computer-use-preview'>().toExtend() - }) - }) - - describe('Detailed property type assertions', () => { - it('all models should have metadata option', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('metadata') - expectTypeOf().toHaveProperty( - 'metadata', - ) - expectTypeOf().toHaveProperty( - 'metadata', - ) - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - >().toHaveProperty('metadata') - }) - - it('all models should have store option', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('store') - expectTypeOf().toHaveProperty( - 'store', - ) - expectTypeOf().toHaveProperty( - 'store', - ) - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - >().toHaveProperty('store') - }) - - it('all models should have service_tier option', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('service_tier') - expectTypeOf().toHaveProperty( - 'service_tier', - ) - expectTypeOf().toHaveProperty( - 'service_tier', - ) - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - >().toHaveProperty('service_tier') - }) - - it('models with tools support should have tool_choice and parallel_tool_calls', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('parallel_tool_calls') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toHaveProperty('tool_choice') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - >().toHaveProperty('tool_choice') - }) - - it('models with structured output should have text option', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1-chat'] - >().toHaveProperty('text') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-chat'] - >().toHaveProperty('text') - }) - - it('models with streaming should have stream_options', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - >().toHaveProperty('stream_options') - }) - }) - - describe('Type discrimination between model categories', () => { - it('full featured models should extend all options', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1-codex'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-pro'] - >().toExtend() - }) - - it('gpt-5-mini and gpt-5-nano should extend FullFeaturedOptions (reasoning + structured output + tools)', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-nano'] - >().toExtend() - }) - - it('standard models should NOT extend reasoning options but should extend structured output and tools', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1-mini'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o-mini'] - >().toExtend() - - // Verify these do NOT extend reasoning options (discrimination already tested above) - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().not.toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().not.toExtend() - }) - - it('reasoning-only models should extend reasoning options but NOT structured output or tools', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3-pro'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3-mini'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o4-mini'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o1-pro'] - >().toExtend() - - // Verify these do NOT extend structured output or tools options - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3'] - >().not.toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3'] - >().not.toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o1'] - >().not.toExtend() - }) - - it('all models should extend base options', () => { - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-mini'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4.1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4o'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o3'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['o1'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-4'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4.0'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5.1-chat'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['gpt-5-chat'] - >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['computer-use-preview'] - >().toExtend() - }) - }) -}) - -// Helper types for message with specific content -type MessageWithContent = { role: 'user'; content: Array } - -/** - * OpenAI Model Input Modality Type Assertions - * - * These tests verify that ConstrainedModelMessage correctly restricts - * content parts based on each model's supported input modalities. - */ -describe('OpenAI Model Input Modality Type Assertions', () => { - // ===== Models with text + image input ===== - - describe('gpt-5.1 (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5.1'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5.1-codex (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5.1-codex'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5 (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5-mini (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5-mini'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5-nano (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5-nano'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5-pro (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5-pro'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-5-codex (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-5-codex'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-4.1 (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-4.1'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-4.1-mini (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-4.1-mini'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-4.1-nano (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-4.1-nano'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('codex-mini-latest (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['codex-mini-latest'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('computer-use-preview (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['computer-use-preview'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o3 (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o3'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o3-pro (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o3-pro'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o3-deep-research (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o3-deep-research'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o4-mini-deep-research (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o4-mini-deep-research'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o4-mini (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o4-mini'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o1 (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o1'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('o1-pro (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o1-pro'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - // ===== Models with text + audio input ===== - - describe('gpt-audio (text + audio)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-audio'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and AudioPart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow ImagePart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - describe('gpt-audio-mini (text + audio)', () => { - type Modalities = OpenAIModelInputModalitiesByName['gpt-audio-mini'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and AudioPart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow ImagePart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - // ===== Models with text only input ===== - - describe('o3-mini (text only)', () => { - type Modalities = OpenAIModelInputModalitiesByName['o3-mini'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart', () => { - expectTypeOf>().toExtend() - }) - - it('should NOT allow ImagePart, AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - expectTypeOf>().not.toExtend() - }) - }) - - // ===== String and null content tests ===== - - describe('String and null content should always be allowed', () => { - it('text+image models should allow string content', () => { - type GPT51Message = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-5.1'] - > - type O3Message = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['o3'] - > - - expectTypeOf<{ role: 'user'; content: string }>().toExtend() - expectTypeOf<{ role: 'user'; content: string }>().toExtend() - }) - - it('text-only models should allow string content', () => { - type O3MiniMessage = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['o3-mini'] - > - - expectTypeOf<{ - role: 'user' - content: string - }>().toExtend() - }) - - it('text+audio models should allow string content', () => { - type GPTAudioMessage = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-audio'] - > - - expectTypeOf<{ - role: 'user' - content: string - }>().toExtend() - }) - - it('all models should allow null content', () => { - type GPT51Message = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-5.1'] - > - type O3MiniMessage = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['o3-mini'] - > - type GPTAudioMessage = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-audio'] - > - - expectTypeOf<{ - role: 'assistant' - content: null - }>().toExtend() - expectTypeOf<{ - role: 'assistant' - content: null - }>().toExtend() - expectTypeOf<{ - role: 'assistant' - content: null - }>().toExtend() - }) - }) - - // ===== Mixed content part validation ===== - - describe('Mixed content part validation', () => { - it('should NOT allow mixing valid and invalid content parts', () => { - type GPT51Message = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-5.1'] - > - - // TextPart + VideoPart should NOT be allowed (GPT-5.1 doesn't support video) - expectTypeOf< - MessageWithContent - >().not.toExtend() - - // ImagePart + AudioPart should NOT be allowed (GPT-5.1 doesn't support audio) - expectTypeOf< - MessageWithContent - >().not.toExtend() - }) - - it('should allow mixing valid content parts', () => { - type GPT51Message = ConstrainedModelMessage< - OpenAIModelInputModalitiesByName['gpt-5.1'] - > - - // TextPart + ImagePart should be allowed - expectTypeOf< - MessageWithContent - >().toExtend() - }) + OpenAIVideoProviderOptions, + OpenAIVideoSize, +} from '../src/video/video-provider-options' + +describe('OpenAI registries', () => { + const expectUnique = (ids: ReadonlyArray) => { + expect(new Set(ids).size).toBe(ids.length) + } + const hasOwn = (object: object, key: PropertyKey) => + Object.prototype.hasOwnProperty.call(object, key) + + it('derives public arrays from the keyed registries', () => { + expect(OPENAI_CHAT_MODELS).toEqual(supportedIds(TEXT_MODELS)) + expect(OPENAI_IMAGE_MODELS).toEqual(supportedIds(IMAGE_MODELS)) + expect(OPENAI_VIDEO_MODELS).toEqual(supportedIds(VIDEO_MODELS)) + expect(OPENAI_TTS_MODELS).toEqual(supportedIds(TTS_MODELS)) + expect(OPENAI_TRANSCRIPTION_MODELS).toEqual(supportedIds(TRANSCRIPTION_MODELS)) + expect(OPENAI_REALTIME_MODELS).toEqual(supportedIds(REALTIME_MODELS)) + }) + + it('derives filtered arrays from lifecycle state', () => { + expect(OPENAI_CURRENT_CHAT_MODELS).toEqual(idsByStatus(TEXT_MODELS, 'active')) + expect(OPENAI_DEPRECATED_CHAT_MODELS).toEqual( + idsByStatus(TEXT_MODELS, 'deprecated'), + ) + expect(OPENAI_PREVIEW_CHAT_MODELS).toEqual(idsByStatus(TEXT_MODELS, 'preview')) + expect(OPENAI_CURRENT_IMAGE_MODELS).toEqual(idsByStatus(IMAGE_MODELS, 'active')) + expect(OPENAI_CURRENT_VIDEO_MODELS).toEqual(idsByStatus(VIDEO_MODELS, 'active')) + expect(OPENAI_CURRENT_TTS_MODELS).toEqual(idsByStatus(TTS_MODELS, 'active')) + expect(OPENAI_CURRENT_TRANSCRIPTION_MODELS).toEqual( + idsByStatus(TRANSCRIPTION_MODELS, 'active'), + ) + }) + + it('surfaces snapshot ids from the registries', () => { + expect(OPENAI_CHAT_SNAPSHOT_MODELS).toEqual(snapshotIds(TEXT_MODELS)) + expect(OPENAI_IMAGE_SNAPSHOT_MODELS).toEqual(snapshotIds(IMAGE_MODELS)) + expect(OPENAI_TTS_SNAPSHOT_MODELS).toEqual(snapshotIds(TTS_MODELS)) + expect(OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS).toEqual( + snapshotIds(TRANSCRIPTION_MODELS), + ) + for (const id of OPENAI_CHAT_SNAPSHOT_MODELS) { + expect(OPENAI_CHAT_MODELS).toContain(id) + expect(hasOwn(TEXT_MODELS, id)).toBe(false) + } + for (const id of OPENAI_IMAGE_SNAPSHOT_MODELS) { + expect(OPENAI_IMAGE_MODELS).toContain(id) + expect(hasOwn(IMAGE_MODELS, id)).toBe(false) + } + for (const id of OPENAI_TTS_SNAPSHOT_MODELS) { + expect(OPENAI_TTS_MODELS).toContain(id) + expect(hasOwn(TTS_MODELS, id)).toBe(false) + } + for (const id of OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS) { + expect(OPENAI_TRANSCRIPTION_MODELS).toContain(id) + expect(hasOwn(TRANSCRIPTION_MODELS, id)).toBe(false) + } + }) + + it('exports deduplicated model lists', () => { + expectUnique(OPENAI_CHAT_MODELS) + expectUnique(OPENAI_CHAT_SNAPSHOT_MODELS) + expectUnique(OPENAI_IMAGE_MODELS) + expectUnique(OPENAI_IMAGE_SNAPSHOT_MODELS) + expectUnique(OPENAI_VIDEO_MODELS) + expectUnique(OPENAI_TTS_MODELS) + expectUnique(OPENAI_TTS_SNAPSHOT_MODELS) + expectUnique(OPENAI_TRANSCRIPTION_MODELS) + expectUnique(OPENAI_TRANSCRIPTION_SNAPSHOT_MODELS) + expectUnique(OPENAI_REALTIME_MODELS) + expectUnique(OPENAI_REALTIME_SNAPSHOT_MODELS) + }) + + it('keeps dead aliases out of the text union', () => { + expect(OPENAI_CHAT_MODELS).not.toContain('chatgpt-4o-latest') + expect(OPENAI_CHAT_MODELS).not.toContain('codex-mini-latest') + }) + + it('keeps codex outputs text-only in the registry', () => { + expect(TEXT_MODELS['gpt-5.3-codex'].output).toEqual(['text']) + expect(TEXT_MODELS['gpt-5.1-codex'].output).toEqual(['text']) + expect(TEXT_MODELS['gpt-5-codex'].output).toEqual(['text']) + }) + + it('keeps audio chat models tool-capable in the feature map', () => { + type Audio15Options = OpenAIChatModelProviderOptionsByName['gpt-audio-1.5'] + type Audio4oOptions = + OpenAIChatModelProviderOptionsByName['gpt-4o-audio-preview'] + type Audio15HasStreaming = Extract + type Audio4oHasStreaming = Extract + + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<'stream_options'>() + }) + + it('keeps gpt-5.4 and computer-use-preview on the intended feature sets', () => { + type GPT54Options = OpenAIChatModelProviderOptionsByName['gpt-5.4'] + type GPT54HasReasoning = Extract + type GPT53ChatOptions = + OpenAIChatModelProviderOptionsByName['gpt-5.3-chat-latest'] + type GPT53ChatHasReasoning = Extract + type GPT5ProOptions = OpenAIChatModelProviderOptionsByName['gpt-5-pro'] + type GPT41Options = OpenAIChatModelProviderOptionsByName['gpt-4.1'] + type GPT41HasReasoning = Extract + type GPT4TurboOptions = OpenAIChatModelProviderOptionsByName['gpt-4-turbo'] + type GPT35TurboOptions = + OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] + type SearchPreviewOptions = + OpenAIChatModelProviderOptionsByName['gpt-4o-search-preview'] + type O1MiniOptions = OpenAIChatModelProviderOptionsByName['o1-mini'] + type O1MiniHasReasoning = Extract + type O3ProOptions = OpenAIChatModelProviderOptionsByName['o3-pro'] + type O3ProHasReasoning = Extract + type ComputerUseOptions = + OpenAIChatModelProviderOptionsByName['computer-use-preview'] + type ComputerUseHasReasoning = Extract + + expectTypeOf().toEqualTypeOf<'reasoning'>() + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().not.toExtend() + expectTypeOf().toExtend() + expectTypeOf().not.toExtend() + expectTypeOf().not.toExtend() + expectTypeOf().toExtend< + OpenAIStructuredOutputOptions + >() + expectTypeOf().not.toExtend() + expectTypeOf().toEqualTypeOf<'reasoning'>() + expectTypeOf().not.toExtend() + expectTypeOf().not.toExtend() + expectTypeOf().toEqualTypeOf<'reasoning'>() + expectTypeOf().toExtend() + expectTypeOf().toExtend() + expectTypeOf().not.toExtend() + expectTypeOf().toEqualTypeOf<'reasoning'>() + expectTypeOf().toExtend() + expectTypeOf().not.toExtend() + }) + + it('keeps reasoning effort and summary unions exact per model', () => { + type GPT51Effort = NonNullable< + NonNullable['effort'] + > + type GPT5Effort = NonNullable< + NonNullable['effort'] + > + type GPT5ProEffort = NonNullable< + NonNullable['effort'] + > + type GPT54ProEffort = NonNullable< + NonNullable< + OpenAIChatModelProviderOptionsByName['gpt-5.4-pro']['reasoning'] + >['effort'] + > + type GPT52ProEffort = NonNullable< + NonNullable< + OpenAIChatModelProviderOptionsByName['gpt-5.2-pro']['reasoning'] + >['effort'] + > + type ComputerUseSummary = NonNullable< + NonNullable< + OpenAIChatModelProviderOptionsByName['computer-use-preview']['reasoning'] + >['summary'] + > + type GPT54Summary = NonNullable< + NonNullable['summary'] + > + + expectTypeOf().toEqualTypeOf<'none' | 'low' | 'medium' | 'high'>() + expectTypeOf().toEqualTypeOf< + 'minimal' | 'low' | 'medium' | 'high' + >() + expectTypeOf().toEqualTypeOf<'high'>() + expectTypeOf().toEqualTypeOf<'medium' | 'high' | 'xhigh'>() + expectTypeOf().toEqualTypeOf<'medium' | 'high' | 'xhigh'>() + expectTypeOf().toEqualTypeOf< + 'auto' | 'detailed' | 'concise' + >() + expectTypeOf().toEqualTypeOf<'auto' | 'detailed'>() + }) + + it('derives modalities and size maps from the registries', () => { + type TextIds = RegistryModelId + type ImageIds = RegistryModelId + type VideoIds = RegistryModelId + type TTSIds = RegistryModelId + type TranscriptionIds = RegistryModelId + + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf< + ImageIds + >() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf< + VideoIds + >() + expectTypeOf().toEqualTypeOf() + }) + + it('keeps modality spot checks on the registry-backed maps', () => { + type GPT54Modalities = OpenAIModelInputModalitiesByName['gpt-5.4'] + type Audio15Modalities = OpenAIModelInputModalitiesByName['gpt-audio-1.5'] + type ImageSizes = OpenAIImageModelSizeByName['gpt-image-1.5'] + type Sora2Sizes = OpenAIVideoModelSizeByName['sora-2'] + type O1PreviewModalities = OpenAIModelInputModalitiesByName['o1-preview'] + type O1MiniModalities = OpenAIModelInputModalitiesByName['o1-mini'] + type O3MiniModalities = OpenAIModelInputModalitiesByName['o3-mini'] + type GPTImageOptions = OpenAIImageModelProviderOptionsByName['gpt-image-1.5'] + type DallE3Options = OpenAIImageModelProviderOptionsByName['dall-e-3'] + type Sora2Options = OpenAIVideoModelProviderOptionsByName['sora-2'] + + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf< + readonly ['text', 'audio'] + >() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf< + '1024x1024' | '1536x1024' | '1024x1536' | 'auto' + >() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + }) + + it('widens the realtime union with supported snapshot-style ids', () => { + type RealtimeIds = RegistryModelId + + expectTypeOf().toEqualTypeOf() + expectTypeOf<'gpt-realtime-1.5'>().toExtend() + expect(OPENAI_REALTIME_MODELS).toEqual(supportedIds(REALTIME_MODELS)) + expect(OPENAI_REALTIME_SNAPSHOT_MODELS).toEqual(snapshotIds(REALTIME_MODELS)) + for (const id of OPENAI_REALTIME_SNAPSHOT_MODELS) { + expect(OPENAI_REALTIME_MODELS).toContain(id) + expect(hasOwn(REALTIME_MODELS, id)).toBe(false) + } + } + expect(OPENAI_REALTIME_MODELS).not.toContain('gpt-4o-realtime') + }) + + it('keeps a few anchor ids on the expected public surfaces', () => { + expect(OPENAI_CHAT_MODELS).toContain('gpt-5.4') + expect(OPENAI_CHAT_MODELS).toContain('gpt-5.3-codex') + expect(OPENAI_CHAT_MODELS).toContain('gpt-4o-audio-preview') + expect(OPENAI_IMAGE_MODELS).toContain('gpt-image-1.5') + expect(OPENAI_TTS_MODELS).toContain('gpt-4o-mini-tts') + expect(OPENAI_REALTIME_MODELS).toContain('gpt-realtime-1.5') + expect(OPENAI_CURRENT_CHAT_MODELS).toContain('gpt-5.4') + expect(OPENAI_DEPRECATED_CHAT_MODELS).toContain('gpt-4.5-preview') + expect(OPENAI_PREVIEW_CHAT_MODELS).toContain('gpt-4o-search-preview') + expect(OPENAI_CURRENT_IMAGE_MODELS).toContain('gpt-image-1.5') + expect(OPENAI_CURRENT_TTS_MODELS).toContain('gpt-4o-mini-tts') + expect(OPENAI_CURRENT_TRANSCRIPTION_MODELS).toContain( + 'gpt-4o-transcribe', + ) + expect(OPENAI_CURRENT_VIDEO_MODELS).toContain('sora-2') }) }) diff --git a/packages/typescript/ai-openai/tests/openai-adapter.test.ts b/packages/typescript/ai-openai/tests/openai-adapter.test.ts index 552793a2e..36ec13907 100644 --- a/packages/typescript/ai-openai/tests/openai-adapter.test.ts +++ b/packages/typescript/ai-openai/tests/openai-adapter.test.ts @@ -129,4 +129,63 @@ describe('OpenAI adapter option mapping', () => { expect(Array.isArray(payload.tools)).toBe(true) expect(payload.tools.length).toBeGreaterThan(0) }) + + it('passes reasoning options through to the Responses API payload', async () => { + const mockStream = createMockChatCompletionsStream([ + { + type: 'response.created', + response: { + id: 'resp-456', + model: 'gpt-5.4', + status: 'in_progress', + created_at: 1234567890, + }, + }, + { + type: 'response.done', + response: { + id: 'resp-456', + model: 'gpt-5.4', + status: 'completed', + created_at: 1234567891, + usage: { + input_tokens: 12, + output_tokens: 4, + }, + }, + }, + ]) + + const responsesCreate = vi.fn().mockResolvedValueOnce(mockStream) + + const adapter = new OpenAITextAdapter({ apiKey: 'test-key' }, 'gpt-5.4') + ;(adapter as any).client = { + responses: { + create: responsesCreate, + }, + } + + for await (const _chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'Think carefully' }], + modelOptions: { + reasoning: { + effort: 'low', + summary: 'detailed', + }, + }, + })) { + // Exhaust the stream so the request is issued. + } + + const [payload] = responsesCreate.mock.calls[0] + expect(payload).toMatchObject({ + model: 'gpt-5.4', + reasoning: { + effort: 'low', + summary: 'detailed', + }, + stream: true, + }) + }) }) diff --git a/packages/typescript/ai-openai/tests/realtime-adapter.test.ts b/packages/typescript/ai-openai/tests/realtime-adapter.test.ts new file mode 100644 index 000000000..6fdc9d0fc --- /dev/null +++ b/packages/typescript/ai-openai/tests/realtime-adapter.test.ts @@ -0,0 +1,116 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { openaiRealtime } from '../src/realtime/adapter' + +class MockDataChannel { + readyState = 'connecting' + onopen: (() => void) | null = null + onmessage: ((event: MessageEvent) => void) | null = null + onerror: ((error: Event) => void) | null = null + send = vi.fn() + close = vi.fn() + + constructor() { + setTimeout(() => { + this.readyState = 'open' + this.onopen?.() + }, 0) + } +} + +class MockRTCPeerConnection { + ontrack: ((event: RTCTrackEvent) => void) | null = null + addTrack = vi.fn() + createOffer = vi.fn().mockResolvedValue({ sdp: 'offer-sdp' }) + setLocalDescription = vi.fn().mockResolvedValue(undefined) + setRemoteDescription = vi.fn().mockResolvedValue(undefined) + close = vi.fn() + + createDataChannel() { + return new MockDataChannel() as unknown as RTCDataChannel + } +} + +class MockAudioContext { + state = 'running' + + createAnalyser() { + return { + fftSize: 2048, + frequencyBinCount: 1024, + smoothingTimeConstant: 0, + getByteFrequencyData: vi.fn(), + getByteTimeDomainData: vi.fn(), + } as unknown as AnalyserNode + } + + createMediaStreamSource() { + return { + connect: vi.fn(), + } as unknown as MediaStreamAudioSourceNode + } + + resume = vi.fn().mockResolvedValue(undefined) + close = vi.fn().mockResolvedValue(undefined) +} + +class MockAudio { + autoplay = false + srcObject: MediaStream | null = null + play = vi.fn().mockResolvedValue(undefined) + pause = vi.fn() +} + +describe('OpenAI realtime adapter', () => { + beforeEach(() => { + const track = { + enabled: true, + stop: vi.fn(), + } + const stream = { + getAudioTracks: () => [track], + getTracks: () => [track], + } + + vi.stubGlobal('RTCPeerConnection', MockRTCPeerConnection) + vi.stubGlobal('AudioContext', MockAudioContext) + vi.stubGlobal('Audio', MockAudio) + vi.stubGlobal('navigator', { + mediaDevices: { + getUserMedia: vi.fn().mockResolvedValue(stream), + }, + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it('defaults WebRTC connections to gpt-realtime-1.5', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response('answer-sdp', { status: 200 }), + ) + + const adapter = openaiRealtime() + const connection = await adapter.connect({ + provider: 'openai', + token: 'ephemeral-token', + expiresAt: Date.now() + 60_000, + config: {}, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://api.openai.com/v1/realtime?model=gpt-realtime-1.5', + { + method: 'POST', + headers: { + Authorization: 'Bearer ephemeral-token', + 'Content-Type': 'application/sdp', + }, + body: 'offer-sdp', + }, + ) + + await connection.disconnect() + }) +}) diff --git a/packages/typescript/ai-openai/tests/realtime-token.test.ts b/packages/typescript/ai-openai/tests/realtime-token.test.ts new file mode 100644 index 000000000..c4f8272d6 --- /dev/null +++ b/packages/typescript/ai-openai/tests/realtime-token.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { openaiRealtimeToken } from '../src/realtime/token' + +describe('OpenAI realtime token adapter', () => { + afterEach(() => { + delete process.env.OPENAI_API_KEY + vi.restoreAllMocks() + }) + + it('defaults session creation to gpt-realtime-1.5', async () => { + process.env.OPENAI_API_KEY = 'test-api-key' + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response( + JSON.stringify({ + client_secret: { + value: 'ephemeral-token', + expires_at: 1_700_000_000, + }, + model: 'gpt-realtime-1.5', + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + + const adapter = openaiRealtimeToken() + const token = await adapter.generateToken() + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://api.openai.com/v1/realtime/sessions', + { + method: 'POST', + headers: { + Authorization: 'Bearer test-api-key', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ model: 'gpt-realtime-1.5' }), + }, + ) + expect(token).toEqual({ + provider: 'openai', + token: 'ephemeral-token', + expiresAt: 1_700_000_000_000, + config: { + model: 'gpt-realtime-1.5', + }, + }) + }) +}) diff --git a/packages/typescript/ai-openai/tests/text-provider-options.test.ts b/packages/typescript/ai-openai/tests/text-provider-options.test.ts new file mode 100644 index 000000000..f36ab0964 --- /dev/null +++ b/packages/typescript/ai-openai/tests/text-provider-options.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { validateTextProviderOptions } from '../src/text/text-provider-options' + +describe('text provider option validation', () => { + it('preserves pre-refactor runtime behavior for reasoning options', () => { + expect(() => + validateTextProviderOptions({ + input: 'hi', + model: 'gpt-4o', + reasoning: { effort: 'low' }, + }), + ).not.toThrow() + + expect(() => + validateTextProviderOptions({ + input: 'hi', + model: 'gpt-5', + reasoning: { effort: 'none' }, + }), + ).not.toThrow() + + expect(() => + validateTextProviderOptions({ + input: 'hi', + model: 'gpt-5.4', + reasoning: { summary: 'concise' }, + }), + ).not.toThrow() + }) + + it('still rejects incompatible conversation fields', () => { + expect(() => + validateTextProviderOptions({ + input: 'hi', + model: 'gpt-4o', + conversation: 'conv_123', + previous_response_id: 'resp_123', + }), + ).toThrow( + "Cannot use both 'conversation' and 'previous_response_id' in the same request.", + ) + }) +}) diff --git a/packages/typescript/ai-openai/tests/transcription-adapter.test.ts b/packages/typescript/ai-openai/tests/transcription-adapter.test.ts new file mode 100644 index 000000000..00d7ab074 --- /dev/null +++ b/packages/typescript/ai-openai/tests/transcription-adapter.test.ts @@ -0,0 +1,151 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createOpenaiTranscription } from '../src/adapters/transcription' + +const stubAdapterClient = ( + adapter: ReturnType, + create: unknown, +) => { + ;(adapter as unknown as { + client: { audio: { transcriptions: { create: unknown } } } + }).client = { + audio: { + transcriptions: { + create, + }, + }, + } +} + +describe('OpenAI transcription adapter', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('defaults non-whisper models to verbose_json and maps rich responses', async () => { + const create = vi.fn().mockResolvedValueOnce({ + text: 'hello world', + language: 'en', + duration: 1.25, + segments: [ + { + id: 1, + start: 0, + end: 1.25, + text: 'hello world', + avg_logprob: -0.5, + }, + ], + words: [{ word: 'hello', start: 0, end: 0.5 }], + }) + + const adapter = createOpenaiTranscription('gpt-4o-transcribe', 'test-api-key') + stubAdapterClient(adapter, create) + + const result = await adapter.transcribe({ + model: 'gpt-4o-transcribe', + audio: new ArrayBuffer(8), + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-4o-transcribe', + response_format: 'verbose_json', + stream: false, + }), + ) + expect(result.text).toBe('hello world') + expect(result.language).toBe('en') + expect(result.duration).toBe(1.25) + expect(result.segments).toEqual([ + { + id: 1, + start: 0, + end: 1.25, + text: 'hello world', + confidence: Math.exp(-0.5), + }, + ]) + expect(result.words).toEqual([{ word: 'hello', start: 0, end: 0.5 }]) + }) + + it('respects explicit whisper response formats and string responses', async () => { + const create = vi.fn().mockResolvedValueOnce('plain transcript') + + const adapter = createOpenaiTranscription('whisper-1', 'test-api-key') + stubAdapterClient(adapter, create) + + const result = await adapter.transcribe({ + model: 'whisper-1', + audio: new ArrayBuffer(8), + language: 'en', + prompt: 'Prefer short phrases', + responseFormat: 'text', + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'whisper-1', + language: 'en', + prompt: 'Prefer short phrases', + response_format: 'text', + stream: false, + }), + ) + expect(result.text).toBe('plain transcript') + expect(result.language).toBe('en') + }) + + it('falls back to an empty string when a non-verbose response has no text field', async () => { + const create = vi.fn().mockResolvedValueOnce({ segments: [] }) + + const adapter = createOpenaiTranscription('gpt-4o-transcribe', 'test-api-key') + stubAdapterClient(adapter, create) + + const result = await adapter.transcribe({ + model: 'gpt-4o-transcribe', + audio: new ArrayBuffer(8), + responseFormat: 'json', + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-4o-transcribe', + response_format: 'json', + stream: false, + }), + ) + expect(result.text).toBe('') + }) + + it('passes modelOptions through to verbose transcription requests', async () => { + const create = vi.fn().mockResolvedValueOnce({ + text: 'hello world', + language: 'en', + duration: 1.25, + segments: [], + words: [], + }) + + const adapter = createOpenaiTranscription('gpt-4o-transcribe', 'test-api-key') + stubAdapterClient(adapter, create) + + await adapter.transcribe({ + model: 'gpt-4o-transcribe', + audio: new ArrayBuffer(8), + modelOptions: { + temperature: 0.3, + timestamp_granularities: ['word', 'segment'], + }, + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-4o-transcribe', + response_format: 'verbose_json', + stream: false, + temperature: 0.3, + timestamp_granularities: ['word', 'segment'], + }), + ) + }) +}) diff --git a/packages/typescript/ai-openai/tests/tts-adapter.test.ts b/packages/typescript/ai-openai/tests/tts-adapter.test.ts new file mode 100644 index 000000000..960f6c4ba --- /dev/null +++ b/packages/typescript/ai-openai/tests/tts-adapter.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from 'vitest' +import { createOpenaiSpeech } from '../src/adapters/tts' + +const stubAdapterClient = ( + adapter: ReturnType, + create: unknown, +) => { + ;(adapter as unknown as { + client: { audio: { speech: { create: unknown } } } + }).client = { + audio: { + speech: { + create, + }, + }, + } +} + +describe('OpenAI TTS adapter', () => { + it('passes supported instructions through and returns mp3 output metadata', async () => { + const create = vi + .fn() + .mockResolvedValueOnce(new Response(Uint8Array.from([1, 2, 3]))) + + const adapter = createOpenaiSpeech('gpt-4o-mini-tts', 'test-api-key') + stubAdapterClient(adapter, create) + + const result = await adapter.generateSpeech({ + model: 'gpt-4o-mini-tts', + text: 'hello world', + modelOptions: { + instructions: 'Speak calmly', + }, + }) + + expect(create).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-4o-mini-tts', + input: 'hello world', + voice: 'alloy', + instructions: 'Speak calmly', + }), + ) + expect(result.model).toBe('gpt-4o-mini-tts') + expect(result.format).toBe('mp3') + expect(result.contentType).toBe('audio/mpeg') + expect(result.audio).toBe(Buffer.from([1, 2, 3]).toString('base64')) + }) + + it('rejects unsupported instructions before calling the API', async () => { + const create = vi.fn() + + const adapter = createOpenaiSpeech('tts-1', 'test-api-key') + stubAdapterClient(adapter, create) + + await expect( + adapter.generateSpeech({ + model: 'tts-1', + text: 'hello world', + modelOptions: { + instructions: 'Speak calmly', + }, + }), + ).rejects.toThrow('The model tts-1 does not support instructions.') + + expect(create).not.toHaveBeenCalled() + }) +}) diff --git a/packages/typescript/ai-openai/tests/video-adapter.test.ts b/packages/typescript/ai-openai/tests/video-adapter.test.ts new file mode 100644 index 000000000..ff191dd83 --- /dev/null +++ b/packages/typescript/ai-openai/tests/video-adapter.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createOpenaiVideo } from '../src/adapters/video' + +describe('OpenAI video adapter', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('uses the configured baseURL for content fallback fetches', async () => { + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce( + new Response(Uint8Array.from([1, 2, 3]), { + status: 200, + headers: { 'content-type': 'video/mp4' }, + }), + ) + + const adapter = createOpenaiVideo('sora-2', 'test-api-key', { + baseURL: 'https://example.test/v1', + }) + ;(adapter as unknown as { client: { videos: { retrieve: (jobId: string) => Promise } } }).client = + { + videos: { + retrieve: vi.fn().mockResolvedValue({}), + }, + } + + const result = await adapter.getVideoUrl('video_123') + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://example.test/v1/videos/video_123/content', + { + method: 'GET', + headers: { + Authorization: 'Bearer test-api-key', + }, + }, + ) + expect(result.jobId).toBe('video_123') + expect(result.url).toMatch(/^data:video\/mp4;base64,/) + }) + + it('lets duration override modelOptions.seconds in create payloads', async () => { + const create = vi.fn().mockResolvedValueOnce({ id: 'video_123' }) + + const adapter = createOpenaiVideo('sora-2', 'test-api-key') + ;(adapter as unknown as { client: { videos: { create: unknown } } }).client = + { + videos: { + create, + }, + } + + const result = await adapter.createVideoJob({ + model: 'sora-2', + prompt: 'A calm lake at sunrise', + duration: 8, + modelOptions: { + size: '720x1280', + seconds: '4', + }, + }) + + expect(create).toHaveBeenCalledWith({ + model: 'sora-2', + prompt: 'A calm lake at sunrise', + size: '720x1280', + seconds: '8', + }) + expect(result).toEqual({ + jobId: 'video_123', + model: 'sora-2', + }) + }) + + it('uses modelOptions.seconds when duration is absent', async () => { + const create = vi.fn().mockResolvedValueOnce({ id: 'video_456' }) + + const adapter = createOpenaiVideo('sora-2', 'test-api-key') + ;(adapter as unknown as { client: { videos: { create: unknown } } }).client = + { + videos: { + create, + }, + } + + await adapter.createVideoJob({ + model: 'sora-2', + prompt: 'A calm lake at sunrise', + modelOptions: { + size: '720x1280', + seconds: '4', + }, + }) + + expect(create).toHaveBeenCalledWith({ + model: 'sora-2', + prompt: 'A calm lake at sunrise', + size: '720x1280', + seconds: '4', + }) + }) +}) diff --git a/packages/typescript/ai-openai/tests/video-provider-options.test.ts b/packages/typescript/ai-openai/tests/video-provider-options.test.ts new file mode 100644 index 000000000..f522adfcb --- /dev/null +++ b/packages/typescript/ai-openai/tests/video-provider-options.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { + toApiSeconds, + validateVideoSeconds, + validateVideoSize, +} from '../src/video/video-provider-options' + +describe('video provider option validation', () => { + it('throws for unknown models', () => { + expect(() => validateVideoSize('not-a-real-model', '1280x720')).toThrow( + 'Unknown video model: not-a-real-model', + ) + expect(() => validateVideoSeconds('not-a-real-model', '4')).toThrow( + 'Unknown video model: not-a-real-model', + ) + }) + + it('accepts valid values for known models', () => { + expect(() => validateVideoSize('sora-2', '1280x720')).not.toThrow() + expect(() => validateVideoSeconds('sora-2', 8)).not.toThrow() + expect(() => validateVideoSeconds('sora-2-pro', '12')).not.toThrow() + }) + + it('rejects invalid values for known models', () => { + expect(() => validateVideoSize('sora-2', '1024x1024')).toThrow( + 'Size "1024x1024" is not supported by model "sora-2".', + ) + expect(() => validateVideoSeconds('sora-2', 6)).toThrow( + 'Duration "6" is not supported by model "sora-2".', + ) + }) + + it('normalizes numeric durations to API strings', () => { + expect(toApiSeconds(8)).toBe('8') + expect(toApiSeconds('12')).toBe('12') + expect(toApiSeconds(undefined)).toBeUndefined() + }) +})