diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index fc3d769ae2..673fcdb4ec 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -7,6 +7,7 @@ import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/ap import { TagMatcher } from "../../utils/tag-matcher" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" +import { getModelParams } from "../transform/model-params" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" @@ -73,7 +74,7 @@ export abstract class BaseOpenAiCompatibleProvider metadata?: ApiHandlerCreateMessageMetadata, requestOptions?: OpenAI.RequestOptions, ) { - const { id: model, info } = this.getModel() + const { id: model, info, reasoning } = this.getModel() // Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply) const max_tokens = @@ -98,8 +99,14 @@ export abstract class BaseOpenAiCompatibleProvider parallel_tool_calls: metadata?.parallelToolCalls ?? true, } - // Add thinking parameter if reasoning is enabled and model supports it - if (this.options.enableReasoningEffort && info.supportsReasoningBinary) { + // Add reasoning_effort from centralized model params (handled by getModelParams) + if (reasoning) { + Object.assign(params, reasoning) + } + + // Fallback: Add binary thinking parameter when reasoning_effort not used but + // reasoning is still enabled and model supports simple on/off thinking + if (!reasoning && this.options.enableReasoningEffort && info.supportsReasoningBinary) { ;(params as any).thinking = { type: "enabled" } } @@ -220,15 +227,20 @@ export abstract class BaseOpenAiCompatibleProvider } async completePrompt(prompt: string): Promise { - const { id: modelId, info: modelInfo } = this.getModel() + const { id: modelId, info: modelInfo, reasoning } = this.getModel() const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = { model: modelId, messages: [{ role: "user", content: prompt }], } - // Add thinking parameter if reasoning is enabled and model supports it - if (this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) { + // Add reasoning_effort from centralized model params + if (reasoning) { + Object.assign(params, reasoning) + } + + // Fallback: Add binary thinking parameter when reasoning_effort not used + if (!reasoning && this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) { ;(params as any).thinking = { type: "enabled" } } @@ -250,11 +262,23 @@ export abstract class BaseOpenAiCompatibleProvider } override getModel() { - const id = + const providerId = this.options.apiModelId && this.options.apiModelId in this.providerModels ? (this.options.apiModelId as ModelName) : this.defaultProviderModelId - return { id, info: this.providerModels[id] } + // Allow user to override model info via openAiCustomModelInfo (like OpenAiHandler), + // enabling support for custom/unknown models with reasoning effort capability + const info: ModelInfo = this.options.openAiCustomModelInfo ?? this.providerModels[providerId] + + const params = getModelParams({ + format: "openai", + modelId: providerId, + model: info, + settings: this.options, + defaultTemperature: this.defaultTemperature, + }) + + return { id: providerId as string, info, ...params } } } diff --git a/src/api/providers/openai-compatible.ts b/src/api/providers/openai-compatible.ts index d129e72452..ac1cae2f13 100644 --- a/src/api/providers/openai-compatible.ts +++ b/src/api/providers/openai-compatible.ts @@ -165,6 +165,12 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si const openAiTools = this.convertToolsForOpenAI(metadata?.tools) const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined + // Build provider options for reasoning_effort (supported by @ai-sdk/openai-compatible) + const modelReasoning = (model as any).reasoning as { reasoning_effort?: string } | undefined + const openaiCompatibleOptions = modelReasoning?.reasoning_effort + ? { reasoningEffort: modelReasoning.reasoning_effort } + : undefined + // Build the request options const requestOptions: Parameters[0] = { model: languageModel, @@ -174,6 +180,9 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si maxOutputTokens: this.getMaxOutputTokens(), tools: aiSdkTools, toolChoice: this.mapToolChoice(metadata?.tool_choice), + ...(openaiCompatibleOptions + ? { providerOptions: { openaiCompatible: openaiCompatibleOptions } as any } + : {}), } // Use streamText for streaming responses @@ -199,12 +208,22 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si */ async completePrompt(prompt: string): Promise { const languageModel = this.getLanguageModel() + const model = this.getModel() + + // Build provider options for reasoning_effort + const modelReasoning = (model as any).reasoning as { reasoning_effort?: string } | undefined + const openaiCompatibleOptions = modelReasoning?.reasoning_effort + ? { reasoningEffort: modelReasoning.reasoning_effort } + : undefined const { text } = await generateText({ model: languageModel, prompt, maxOutputTokens: this.getMaxOutputTokens(), temperature: this.config.temperature ?? 0, + ...(openaiCompatibleOptions + ? { providerOptions: { openaiCompatible: openaiCompatibleOptions } as any } + : {}), }) return text