diff --git a/.changeset/brave-impalas-pull.md b/.changeset/brave-impalas-pull.md new file mode 100644 index 000000000000..7e126db9833c --- /dev/null +++ b/.changeset/brave-impalas-pull.md @@ -0,0 +1,10 @@ +--- +'@ai-sdk/openai-compatible': patch +--- + +Refine OpenAI-compatible chat option handling by separating stable options from legacy normalized compatibility options. + +- Keep `user` as the stable normalized chat option. +- Continue supporting `reasoningEffort`, `textVerbosity`, and `strictJsonSchema` for backwards compatibility. +- Emit deprecation warnings when legacy normalized options are used and recommend provider-native fields. +- Update provider docs to clarify the normalized compatibility layer and preferred provider-native configuration. diff --git a/content/providers/02-openai-compatible-providers/index.mdx b/content/providers/02-openai-compatible-providers/index.mdx index d663a6ed2167..0e177bd1336f 100644 --- a/content/providers/02-openai-compatible-providers/index.mdx +++ b/content/providers/02-openai-compatible-providers/index.mdx @@ -423,27 +423,13 @@ const { text } = await generateText({ ## Chat Model Options -The following provider options are available for chat models via `providerOptions`: +The following provider option is available for chat models via `providerOptions`: - **user** _string_ A unique identifier representing your end-user, which can help the provider to monitor and detect abuse. -- **reasoningEffort** _string_ - - Reasoning effort for reasoning models. The exact values depend on the provider. - -- **textVerbosity** _string_ - - Controls the verbosity of the generated text. The exact values depend on the provider. - -- **strictJsonSchema** _boolean_ - - Whether to use strict JSON schema validation. When true, the model uses constrained - decoding to guarantee schema compliance. Only used when the provider supports - structured outputs and a schema is provided. Defaults to `true`. - ```ts const { text } = await generateText({ model: provider('model-id'), @@ -457,6 +443,16 @@ const { text } = await generateText({ }); ``` +### Legacy normalized options (deprecated) + +The following normalized chat options are still supported for compatibility, but are deprecated: + +- `reasoningEffort` +- `textVerbosity` +- `strictJsonSchema` + +Prefer provider-native fields directly in `providerOptions.providerName` for long-term compatibility. + ## Provider-specific options The OpenAI Compatible provider supports adding provider-specific options to the request body. These are specified with the `providerOptions` field in the request body. diff --git a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts index ee71b886e191..f43d36f77283 100644 --- a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts +++ b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts @@ -469,6 +469,38 @@ describe('doGenerate', () => { }); }); + it('should keep normalized chat options for compatibility and emit deprecation warning', async () => { + prepareJsonResponse(); + + const result = await provider('grok-beta').doGenerate({ + prompt: TEST_PROMPT, + providerOptions: { + 'test-provider': { + reasoningEffort: 'high', + }, + }, + }); + + expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(` + { + "messages": [ + { + "content": "Hello", + "role": "user", + }, + ], + "model": "grok-beta", + "reasoning_effort": "high", + } + `); + + expect(result.warnings).toContainEqual({ + type: 'other', + message: + "The normalized OpenAI-compatible chat options ('reasoningEffort', 'textVerbosity', 'strictJsonSchema') are deprecated. Use provider-native fields in providerOptions.test-provider instead.", + }); + }); + it('should include provider-specific options', async () => { prepareJsonResponse(); diff --git a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts index 9a803cb1173e..15d9c0745bc7 100644 --- a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts +++ b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts @@ -35,11 +35,17 @@ import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason'; import { OpenAICompatibleChatModelId, + openaiCompatibleLanguageModelChatLegacyOptions, openaiCompatibleLanguageModelChatOptions, } from './openai-compatible-chat-options'; import { MetadataExtractor } from './openai-compatible-metadata-extractor'; import { prepareTools } from './openai-compatible-prepare-tools'; +const compatibilityOptionKeys = new Set([ + ...Object.keys(openaiCompatibleLanguageModelChatOptions.shape), + ...Object.keys(openaiCompatibleLanguageModelChatLegacyOptions.shape), +]); + export type OpenAICompatibleChatConfig = { provider: string; headers: () => Record; @@ -129,34 +135,70 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { const warnings: SharedV3Warning[] = []; // Parse provider options - check for deprecated 'openai-compatible' key - const deprecatedOptions = await parseProviderOptions({ + const deprecatedBaseOptions = await parseProviderOptions({ provider: 'openai-compatible', providerOptions, schema: openaiCompatibleLanguageModelChatOptions, }); + const deprecatedLegacyOptions = await parseProviderOptions({ + provider: 'openai-compatible', + providerOptions, + schema: openaiCompatibleLanguageModelChatLegacyOptions, + }); - if (deprecatedOptions != null) { + if (deprecatedBaseOptions != null || deprecatedLegacyOptions != null) { warnings.push({ type: 'other', message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`, }); } + const openaiCompatibleBaseOptions = await parseProviderOptions({ + provider: 'openaiCompatible', + providerOptions, + schema: openaiCompatibleLanguageModelChatOptions, + }); + + const openaiCompatibleLegacyOptions = await parseProviderOptions({ + provider: 'openaiCompatible', + providerOptions, + schema: openaiCompatibleLanguageModelChatLegacyOptions, + }); + + const providerBaseOptions = await parseProviderOptions({ + provider: this.providerOptionsName, + providerOptions, + schema: openaiCompatibleLanguageModelChatOptions, + }); + + const providerLegacyOptions = await parseProviderOptions({ + provider: this.providerOptionsName, + providerOptions, + schema: openaiCompatibleLanguageModelChatLegacyOptions, + }); + const compatibleOptions = Object.assign( - deprecatedOptions ?? {}, - (await parseProviderOptions({ - provider: 'openaiCompatible', - providerOptions, - schema: openaiCompatibleLanguageModelChatOptions, - })) ?? {}, - (await parseProviderOptions({ - provider: this.providerOptionsName, - providerOptions, - schema: openaiCompatibleLanguageModelChatOptions, - })) ?? {}, + deprecatedBaseOptions ?? {}, + openaiCompatibleBaseOptions ?? {}, + providerBaseOptions ?? {}, ); + const legacyCompatibilityOptions = Object.assign( + deprecatedLegacyOptions ?? {}, + openaiCompatibleLegacyOptions ?? {}, + providerLegacyOptions ?? {}, + ); + + if (Object.keys(legacyCompatibilityOptions).length > 0) { + warnings.push({ + type: 'other', + message: + `The normalized OpenAI-compatible chat options ('reasoningEffort', 'textVerbosity', 'strictJsonSchema') are deprecated. ` + + `Use provider-native fields in providerOptions.${this.providerOptionsName} instead.`, + }); + } - const strictJsonSchema = compatibleOptions?.strictJsonSchema ?? true; + const strictJsonSchema = + legacyCompatibilityOptions.strictJsonSchema ?? true; if (topK != null) { warnings.push({ type: 'unsupported', feature: 'topK' }); @@ -219,16 +261,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { ...Object.fromEntries( Object.entries( providerOptions?.[this.providerOptionsName] ?? {}, - ).filter( - ([key]) => - !Object.keys( - openaiCompatibleLanguageModelChatOptions.shape, - ).includes(key), - ), + ).filter(([key]) => !compatibilityOptionKeys.has(key)), ), - reasoning_effort: compatibleOptions.reasoningEffort, - verbosity: compatibleOptions.textVerbosity, + reasoning_effort: legacyCompatibilityOptions.reasoningEffort, + verbosity: legacyCompatibilityOptions.textVerbosity, // messages: messages: convertToOpenAICompatibleChatMessages(prompt), diff --git a/packages/openai-compatible/src/chat/openai-compatible-chat-options.ts b/packages/openai-compatible/src/chat/openai-compatible-chat-options.ts index 035ef5a0324d..3ec32c41246b 100644 --- a/packages/openai-compatible/src/chat/openai-compatible-chat-options.ts +++ b/packages/openai-compatible/src/chat/openai-compatible-chat-options.ts @@ -8,23 +8,26 @@ export const openaiCompatibleLanguageModelChatOptions = z.object({ * monitor and detect abuse. */ user: z.string().optional(), +}); +/** + * Legacy normalized chat options kept for backwards compatibility. + * + * Prefer provider-native option names passed through `providerOptions[providerName]`. + */ +export const openaiCompatibleLanguageModelChatLegacyOptions = z.object({ /** - * Reasoning effort for reasoning models. Defaults to `medium`. + * @deprecated Use provider-native request fields for reasoning configuration. */ reasoningEffort: z.string().optional(), /** - * Controls the verbosity of the generated text. Defaults to `medium`. + * @deprecated Use provider-native request fields for verbosity configuration. */ textVerbosity: z.string().optional(), /** - * Whether to use strict JSON schema validation. - * When true, the model uses constrained decoding to guarantee schema compliance. - * Only used when the provider supports structured outputs and a schema is provided. - * - * @default true + * @deprecated Use provider-native request fields for schema strictness configuration. */ strictJsonSchema: z.boolean().optional(), }); @@ -32,3 +35,7 @@ export const openaiCompatibleLanguageModelChatOptions = z.object({ export type OpenAICompatibleLanguageModelChatOptions = z.infer< typeof openaiCompatibleLanguageModelChatOptions >; + +export type OpenAICompatibleLanguageModelChatLegacyOptions = z.infer< + typeof openaiCompatibleLanguageModelChatLegacyOptions +>;