diff --git a/packages/shared/aiCore/config/aihubmix.ts b/packages/shared/aiCore/config/aihubmix.ts
index 182e4fb194..21b875da34 100644
--- a/packages/shared/aiCore/config/aihubmix.ts
+++ b/packages/shared/aiCore/config/aihubmix.ts
@@ -17,7 +17,7 @@ const extraProviderConfig =
(provider: P) => {
}
}
-function isOpenAILLMModel(model: M): boolean {
+export function isOpenAILLMModel(model: M): boolean {
const modelId = getLowerBaseModelName(model.id)
const reasonings = ['o1', 'o3', 'o4', 'gpt-oss']
if (reasonings.some((r) => modelId.includes(r))) {
diff --git a/src/main/apiServer/adapters/converters/OpenAIResponsesMessageConverter.ts b/src/main/apiServer/adapters/converters/OpenAIResponsesMessageConverter.ts
new file mode 100644
index 0000000000..5ffea3b99b
--- /dev/null
+++ b/src/main/apiServer/adapters/converters/OpenAIResponsesMessageConverter.ts
@@ -0,0 +1,262 @@
+/**
+ * OpenAI Responses API Message Converter
+ *
+ * Converts OpenAI Responses API format to AI SDK format.
+ * Uses types from @cherrystudio/openai SDK.
+ */
+
+import type { ProviderOptions, ToolCallPart, ToolResultPart } from '@ai-sdk/provider-utils'
+import type OpenAI from '@cherrystudio/openai'
+import type { Provider } from '@types'
+import type { ImagePart, ModelMessage, TextPart, Tool as AiSdkTool } from 'ai'
+import { tool, zodSchema } from 'ai'
+
+import type { IMessageConverter, StreamTextOptions } from '../interfaces'
+import { type JsonSchemaLike, jsonSchemaToZod } from './json-schema-to-zod'
+import type { ReasoningEffort } from './provider-options-mapper'
+import { mapReasoningEffortToProviderOptions } from './provider-options-mapper'
+
+// SDK types
+type ResponseCreateParams = OpenAI.Responses.ResponseCreateParams
+type EasyInputMessage = OpenAI.Responses.EasyInputMessage
+type FunctionTool = OpenAI.Responses.FunctionTool
+type ResponseInputText = OpenAI.Responses.ResponseInputText
+type ResponseInputImage = OpenAI.Responses.ResponseInputImage
+
+/**
+ * Extended ResponseCreateParams with reasoning_effort
+ */
+export type ResponsesCreateParams = ResponseCreateParams & {
+ reasoning_effort?: ReasoningEffort
+}
+
+/**
+ * OpenAI Responses Message Converter
+ */
+export class OpenAIResponsesMessageConverter implements IMessageConverter {
+ /**
+ * Convert Responses API params to AI SDK ModelMessage[]
+ */
+ toAiSdkMessages(params: ResponsesCreateParams): ModelMessage[] {
+ const messages: ModelMessage[] = []
+
+ // Add instructions as system message if present
+ if (params.instructions && typeof params.instructions === 'string') {
+ messages.push({ role: 'system', content: params.instructions })
+ }
+
+ // Handle no input
+ if (!params.input) {
+ return messages
+ }
+
+ // Handle string input
+ if (typeof params.input === 'string') {
+ messages.push({ role: 'user', content: params.input })
+ return messages
+ }
+
+ // Handle message array input
+ const inputArray = params.input
+
+ // Build tool call ID to name mapping for tool results
+ const toolCallIdToName = new Map()
+ for (const item of inputArray) {
+ // Handle ResponseFunctionToolCall
+ if ('type' in item && item.type === 'function_call' && 'call_id' in item && 'name' in item) {
+ const funcCall = item as OpenAI.Responses.ResponseFunctionToolCall
+ toolCallIdToName.set(funcCall.call_id, funcCall.name)
+ }
+ }
+
+ for (const item of inputArray) {
+ const converted = this.convertInputItem(item, toolCallIdToName)
+ if (converted.length > 0) {
+ messages.push(...converted)
+ }
+ }
+
+ return messages
+ }
+
+ /**
+ * Convert a single input item to AI SDK message(s)
+ */
+ private convertInputItem(
+ item: OpenAI.Responses.ResponseInputItem,
+ toolCallIdToName: Map
+ ): ModelMessage[] {
+ // Handle EasyInputMessage (has role and content)
+ if ('role' in item && 'content' in item) {
+ return this.convertEasyInputMessage(item as EasyInputMessage)
+ }
+
+ // Handle function_call_output
+ if ('type' in item && item.type === 'function_call_output') {
+ const output = item as OpenAI.Responses.ResponseInputItem.FunctionCallOutput
+ const outputStr = typeof output.output === 'string' ? output.output : JSON.stringify(output.output)
+ return this.convertFunctionCallOutput(output.call_id, outputStr, toolCallIdToName)
+ }
+
+ return []
+ }
+
+ /**
+ * Convert EasyInputMessage to AI SDK message
+ */
+ private convertEasyInputMessage(msg: EasyInputMessage): ModelMessage[] {
+ switch (msg.role) {
+ case 'developer':
+ case 'system':
+ return this.convertSystemMessage(msg.content)
+ case 'user':
+ return this.convertUserMessage(msg.content)
+ case 'assistant':
+ return this.convertAssistantMessage(msg.content)
+ default:
+ return []
+ }
+ }
+
+ /**
+ * Convert system message content
+ */
+ private convertSystemMessage(content: EasyInputMessage['content']): ModelMessage[] {
+ if (typeof content === 'string') {
+ return [{ role: 'system', content }]
+ }
+
+ // Array content - extract text from input_text parts
+ const textParts: string[] = []
+ for (const part of content) {
+ if (part.type === 'input_text') {
+ textParts.push((part as ResponseInputText).text)
+ }
+ }
+
+ if (textParts.length > 0) {
+ return [{ role: 'system', content: textParts.join('\n') }]
+ }
+
+ return []
+ }
+
+ /**
+ * Convert user message content
+ */
+ private convertUserMessage(content: EasyInputMessage['content']): ModelMessage[] {
+ if (typeof content === 'string') {
+ return [{ role: 'user', content }]
+ }
+
+ const parts: (TextPart | ImagePart)[] = []
+
+ for (const part of content) {
+ if (part.type === 'input_text') {
+ parts.push({ type: 'text', text: (part as ResponseInputText).text })
+ } else if (part.type === 'input_image') {
+ const img = part as ResponseInputImage
+ if (img.image_url) {
+ parts.push({ type: 'image', image: img.image_url })
+ }
+ }
+ }
+
+ if (parts.length > 0) {
+ return [{ role: 'user', content: parts }]
+ }
+
+ return []
+ }
+
+ /**
+ * Convert assistant message content
+ */
+ private convertAssistantMessage(content: EasyInputMessage['content']): ModelMessage[] {
+ const parts: (TextPart | ToolCallPart)[] = []
+
+ if (typeof content === 'string') {
+ parts.push({ type: 'text', text: content })
+ } else {
+ for (const part of content) {
+ // input_text can appear in assistant messages in conversation history
+ if (part.type === 'input_text') {
+ parts.push({ type: 'text', text: (part as ResponseInputText).text })
+ }
+ }
+ }
+
+ if (parts.length > 0) {
+ return [{ role: 'assistant', content: parts }]
+ }
+
+ return []
+ }
+
+ /**
+ * Convert function call output to tool result
+ */
+ private convertFunctionCallOutput(
+ callId: string,
+ output: string,
+ toolCallIdToName: Map
+ ): ModelMessage[] {
+ const toolName = toolCallIdToName.get(callId) || 'unknown'
+
+ const toolResultPart: ToolResultPart = {
+ type: 'tool-result',
+ toolCallId: callId,
+ toolName,
+ output: { type: 'text', value: output }
+ }
+
+ return [{ role: 'tool', content: [toolResultPart] }]
+ }
+
+ /**
+ * Convert Responses API tools to AI SDK tools
+ */
+ toAiSdkTools(params: ResponsesCreateParams): Record | undefined {
+ const tools = params.tools
+ if (!tools || tools.length === 0) return undefined
+
+ const aiSdkTools: Record = {}
+
+ for (const toolDef of tools) {
+ if (toolDef.type !== 'function') continue
+
+ const funcTool = toolDef as FunctionTool
+ const rawSchema = funcTool.parameters
+ const schema = rawSchema ? jsonSchemaToZod(rawSchema as JsonSchemaLike) : jsonSchemaToZod({ type: 'object' })
+
+ const aiTool = tool({
+ description: funcTool.description || '',
+ inputSchema: zodSchema(schema)
+ })
+
+ aiSdkTools[funcTool.name] = aiTool
+ }
+
+ return Object.keys(aiSdkTools).length > 0 ? aiSdkTools : undefined
+ }
+
+ /**
+ * Extract stream/generation options from Responses API params
+ */
+ extractStreamOptions(params: ResponsesCreateParams): StreamTextOptions {
+ return {
+ maxOutputTokens: params.max_output_tokens ?? undefined,
+ temperature: params.temperature ?? undefined,
+ topP: params.top_p ?? undefined
+ }
+ }
+
+ /**
+ * Extract provider-specific options from Responses API params
+ */
+ extractProviderOptions(provider: Provider, params: ResponsesCreateParams): ProviderOptions | undefined {
+ return mapReasoningEffortToProviderOptions(provider, params.reasoning_effort)
+ }
+}
+
+export default OpenAIResponsesMessageConverter
diff --git a/src/main/apiServer/adapters/converters/provider-options-mapper.ts b/src/main/apiServer/adapters/converters/provider-options-mapper.ts
index c9a0ea99d5..4289d60b29 100644
--- a/src/main/apiServer/adapters/converters/provider-options-mapper.ts
+++ b/src/main/apiServer/adapters/converters/provider-options-mapper.ts
@@ -20,6 +20,9 @@ import type { ProviderOptions } from '@ai-sdk/provider-utils'
import type { XaiProviderOptions } from '@ai-sdk/xai'
import type { MessageCreateParams } from '@anthropic-ai/sdk/resources/messages'
import type { ReasoningEffort } from '@cherrystudio/openai/resources'
+
+// Re-export for use by message converters
+export type { ReasoningEffort }
import type { OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider'
import { SystemProviderIds } from '@shared/types'
import { isAnthropicProvider, isAwsBedrockProvider, isGeminiProvider, isOpenAIProvider } from '@shared/utils/provider'
diff --git a/src/main/apiServer/adapters/factory/MessageConverterFactory.ts b/src/main/apiServer/adapters/factory/MessageConverterFactory.ts
index 9513bc9462..8d1fa18d66 100644
--- a/src/main/apiServer/adapters/factory/MessageConverterFactory.ts
+++ b/src/main/apiServer/adapters/factory/MessageConverterFactory.ts
@@ -9,6 +9,10 @@ import type { MessageCreateParams } from '@anthropic-ai/sdk/resources/messages'
import { AnthropicMessageConverter, type ReasoningCache } from '../converters/AnthropicMessageConverter'
import { type ExtendedChatCompletionCreateParams, OpenAIMessageConverter } from '../converters/OpenAIMessageConverter'
+import {
+ OpenAIResponsesMessageConverter,
+ type ResponsesCreateParams
+} from '../converters/OpenAIResponsesMessageConverter'
import type { IMessageConverter, InputFormat } from '../interfaces'
/**
@@ -17,6 +21,7 @@ import type { IMessageConverter, InputFormat } from '../interfaces'
export type InputParamsMap = {
openai: ExtendedChatCompletionCreateParams
anthropic: MessageCreateParams
+ 'openai-responses': ResponsesCreateParams
}
/**
@@ -58,6 +63,9 @@ export class MessageConverterFactory {
if (format === 'openai') {
return new OpenAIMessageConverter() as IMessageConverter
}
+ if (format === 'openai-responses') {
+ return new OpenAIResponsesMessageConverter() as IMessageConverter
+ }
return new AnthropicMessageConverter({
googleReasoningCache: options.googleReasoningCache,
openRouterReasoningCache: options.openRouterReasoningCache
@@ -68,14 +76,14 @@ export class MessageConverterFactory {
* Check if a format is supported
*/
static supportsFormat(format: string): format is InputFormat {
- return format === 'openai' || format === 'anthropic'
+ return format === 'openai' || format === 'anthropic' || format === 'openai-responses'
}
/**
* Get list of all supported formats
*/
static getSupportedFormats(): InputFormat[] {
- return ['openai', 'anthropic']
+ return ['openai', 'anthropic', 'openai-responses']
}
}
diff --git a/src/main/apiServer/adapters/factory/StreamAdapterFactory.ts b/src/main/apiServer/adapters/factory/StreamAdapterFactory.ts
index 3f686de44d..a184657a27 100644
--- a/src/main/apiServer/adapters/factory/StreamAdapterFactory.ts
+++ b/src/main/apiServer/adapters/factory/StreamAdapterFactory.ts
@@ -6,9 +6,11 @@
*/
import { AnthropicSSEFormatter } from '../formatters/AnthropicSSEFormatter'
+import { OpenAIResponsesSSEFormatter } from '../formatters/OpenAIResponsesSSEFormatter'
import { OpenAISSEFormatter } from '../formatters/OpenAISSEFormatter'
import type { ISSEFormatter, IStreamAdapter, OutputFormat, StreamAdapterOptions } from '../interfaces'
import { AiSdkToAnthropicSSE } from '../stream/AiSdkToAnthropicSSE'
+import { AiSdkToOpenAIResponsesSSE } from '../stream/AiSdkToOpenAIResponsesSSE'
import { AiSdkToOpenAISSE } from '../stream/AiSdkToOpenAISSE'
/**
@@ -51,6 +53,13 @@ export class StreamAdapterFactory {
adapterClass: AiSdkToOpenAISSE,
formatterClass: OpenAISSEFormatter
}
+ ],
+ [
+ 'openai-responses',
+ {
+ adapterClass: AiSdkToOpenAIResponsesSSE,
+ formatterClass: OpenAIResponsesSSEFormatter
+ }
]
])
diff --git a/src/main/apiServer/adapters/formatters/OpenAIResponsesSSEFormatter.ts b/src/main/apiServer/adapters/formatters/OpenAIResponsesSSEFormatter.ts
new file mode 100644
index 0000000000..004dfdb02c
--- /dev/null
+++ b/src/main/apiServer/adapters/formatters/OpenAIResponsesSSEFormatter.ts
@@ -0,0 +1,50 @@
+/**
+ * OpenAI Responses API SSE Formatter
+ *
+ * Formats OpenAI Responses API stream events for Server-Sent Events.
+ * Responses API uses named events with semantic types:
+ * - event: {type}\n
+ * - data: {json}\n\n
+ *
+ * @see https://platform.openai.com/docs/api-reference/responses-streaming
+ */
+
+import type OpenAI from '@cherrystudio/openai'
+
+import type { ISSEFormatter } from '../interfaces'
+
+/**
+ * Use SDK type for ResponseStreamEvent
+ */
+type ResponseStreamEvent = OpenAI.Responses.ResponseStreamEvent
+
+/**
+ * OpenAI Responses API SSE Formatter
+ *
+ * Unlike Chat Completions API which uses only `data:` lines,
+ * Responses API uses named events with `event:` and `data:` lines.
+ */
+export class OpenAIResponsesSSEFormatter implements ISSEFormatter {
+ /**
+ * Format a Responses API event for SSE streaming
+ *
+ * @example
+ * event: response.created
+ * data: {"type":"response.created","response":{...}}
+ *
+ * event: response.output_text.delta
+ * data: {"type":"response.output_text.delta","delta":"Hello"}
+ */
+ formatEvent(event: ResponseStreamEvent): string {
+ return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`
+ }
+
+ /**
+ * Format the stream termination marker
+ */
+ formatDone(): string {
+ return 'data: [DONE]\n\n'
+ }
+}
+
+export default OpenAIResponsesSSEFormatter
diff --git a/src/main/apiServer/adapters/index.ts b/src/main/apiServer/adapters/index.ts
index 410aebc3b0..eefac4e064 100644
--- a/src/main/apiServer/adapters/index.ts
+++ b/src/main/apiServer/adapters/index.ts
@@ -12,6 +12,7 @@
// Stream Adapters
export { AiSdkToAnthropicSSE } from './stream/AiSdkToAnthropicSSE'
+export { AiSdkToOpenAIResponsesSSE } from './stream/AiSdkToOpenAIResponsesSSE'
export { AiSdkToOpenAISSE } from './stream/AiSdkToOpenAISSE'
export { BaseStreamAdapter } from './stream/BaseStreamAdapter'
@@ -19,9 +20,14 @@ export { BaseStreamAdapter } from './stream/BaseStreamAdapter'
export { AnthropicMessageConverter, type ReasoningCache } from './converters/AnthropicMessageConverter'
export { type JsonSchemaLike, jsonSchemaToZod } from './converters/json-schema-to-zod'
export { type ExtendedChatCompletionCreateParams, OpenAIMessageConverter } from './converters/OpenAIMessageConverter'
+export {
+ OpenAIResponsesMessageConverter,
+ type ResponsesCreateParams
+} from './converters/OpenAIResponsesMessageConverter'
// SSE Formatters
export { AnthropicSSEFormatter } from './formatters/AnthropicSSEFormatter'
+export { OpenAIResponsesSSEFormatter } from './formatters/OpenAIResponsesSSEFormatter'
export { type ChatCompletionChunk, OpenAISSEFormatter } from './formatters/OpenAISSEFormatter'
// Factory
diff --git a/src/main/apiServer/adapters/interfaces.ts b/src/main/apiServer/adapters/interfaces.ts
index b4b4f7b93c..2b2ba19232 100644
--- a/src/main/apiServer/adapters/interfaces.ts
+++ b/src/main/apiServer/adapters/interfaces.ts
@@ -19,7 +19,7 @@ export type OutputFormat = 'anthropic' | 'openai' | 'gemini' | 'openai-responses
/**
* Supported input formats for message converters
*/
-export type InputFormat = 'anthropic' | 'openai'
+export type InputFormat = 'anthropic' | 'openai' | 'openai-responses'
/**
* Stream text options extracted from input params
diff --git a/src/main/apiServer/adapters/stream/AiSdkToOpenAIResponsesSSE.ts b/src/main/apiServer/adapters/stream/AiSdkToOpenAIResponsesSSE.ts
new file mode 100644
index 0000000000..d02cde5f48
--- /dev/null
+++ b/src/main/apiServer/adapters/stream/AiSdkToOpenAIResponsesSSE.ts
@@ -0,0 +1,420 @@
+/**
+ * AI SDK to OpenAI Responses API SSE Adapter
+ *
+ * Converts AI SDK's fullStream (TextStreamPart) events to OpenAI Responses API SSE format.
+ * This adapter emits semantic events like:
+ * - response.created
+ * - response.in_progress
+ * - response.output_item.added
+ * - response.content_part.added
+ * - response.output_text.delta
+ * - response.output_text.done
+ * - response.completed
+ *
+ * @see https://platform.openai.com/docs/api-reference/responses-streaming
+ */
+
+import type OpenAI from '@cherrystudio/openai'
+import { loggerService } from '@logger'
+import type { FinishReason, LanguageModelUsage, TextStreamPart, ToolSet } from 'ai'
+
+import type { StreamAdapterOptions } from '../interfaces'
+import { BaseStreamAdapter } from './BaseStreamAdapter'
+
+const logger = loggerService.withContext('AiSdkToOpenAIResponsesSSE')
+
+/**
+ * Use SDK types for events
+ */
+type Response = OpenAI.Responses.Response
+type ResponseStreamEvent = OpenAI.Responses.ResponseStreamEvent
+type ResponseUsage = OpenAI.Responses.ResponseUsage
+type ResponseOutputMessage = OpenAI.Responses.ResponseOutputMessage
+type ResponseOutputText = OpenAI.Responses.ResponseOutputText
+
+/**
+ * Minimal response fields required for streaming.
+ * Uses Pick to select only necessary fields from the full Response type.
+ */
+type StreamingResponseFields = Pick
+
+/**
+ * Partial response type for streaming that includes optional usage.
+ * During streaming, we only emit a subset of fields.
+ */
+type PartialStreamingResponse = StreamingResponseFields & {
+ usage?: Partial
+}
+
+/**
+ * Minimal usage type for streaming responses.
+ * The SDK's ResponseUsage requires input_tokens_details and output_tokens_details,
+ * but during streaming we may only have the basic token counts.
+ */
+type StreamingUsage = Pick
+
+/**
+ * OpenAI Responses finish reasons
+ */
+type ResponsesFinishReason = 'stop' | 'max_output_tokens' | 'content_filter' | 'tool_calls' | 'cancelled' | null
+
+/**
+ * Tool call state for tracking
+ */
+interface ToolCallState {
+ index: number
+ callId: string
+ name: string
+ arguments: string
+}
+
+/**
+ * Adapter that converts AI SDK fullStream events to OpenAI Responses API SSE events
+ */
+export class AiSdkToOpenAIResponsesSSE extends BaseStreamAdapter {
+ private createdAt: number
+ private sequenceNumber = 0
+ private toolCalls: Map = new Map()
+ private currentToolCallIndex = 0
+ private finishReason: ResponsesFinishReason = null
+ private textContent = ''
+ private outputItemId: string
+ private contentPartIndex = 0
+
+ constructor(options: StreamAdapterOptions) {
+ super(options)
+ this.createdAt = Math.floor(Date.now() / 1000)
+ this.outputItemId = `msg_${this.state.messageId}`
+ }
+
+ /**
+ * Get next sequence number
+ */
+ private nextSequence(): number {
+ return this.sequenceNumber++
+ }
+
+ /**
+ * Build base response object for streaming events.
+ * Returns a partial response with only the fields needed for streaming.
+ * Cast to Response for SDK compatibility - streaming events intentionally
+ * omit fields that are not available until completion.
+ */
+ private buildBaseResponse(status: 'in_progress' | 'completed' | 'failed' = 'in_progress'): PartialStreamingResponse {
+ return {
+ id: `resp_${this.state.messageId}`,
+ object: 'response',
+ created_at: this.createdAt,
+ status,
+ model: this.state.model,
+ output: [],
+ usage: this.buildUsage()
+ }
+ }
+
+ /**
+ * Build usage object for streaming responses.
+ * Uses StreamingUsage which only includes basic token counts,
+ * omitting the detailed breakdowns (input_tokens_details, output_tokens_details)
+ * that are not available during streaming.
+ */
+ private buildUsage(): StreamingUsage {
+ return {
+ input_tokens: this.state.inputTokens,
+ output_tokens: this.state.outputTokens,
+ total_tokens: this.state.inputTokens + this.state.outputTokens
+ }
+ }
+
+ /**
+ * Build base response and cast to Response for event emission.
+ * This is safe because streaming consumers expect partial data.
+ */
+ private buildResponseForEvent(status: 'in_progress' | 'completed' | 'failed' = 'in_progress'): Response {
+ return this.buildBaseResponse(status) as Response
+ }
+
+ /**
+ * Emit the initial message start events
+ */
+ protected emitMessageStart(): void {
+ if (this.state.hasEmittedMessageStart) return
+ this.state.hasEmittedMessageStart = true
+
+ // Emit response.created
+ const createdEvent: ResponseStreamEvent = {
+ type: 'response.created',
+ response: this.buildResponseForEvent('in_progress'),
+ sequence_number: this.nextSequence()
+ }
+ this.emit(createdEvent)
+
+ // Emit response.in_progress
+ const inProgressEvent: ResponseStreamEvent = {
+ type: 'response.in_progress',
+ response: this.buildResponseForEvent('in_progress'),
+ sequence_number: this.nextSequence()
+ }
+ this.emit(inProgressEvent)
+
+ // Emit output_item.added for the message
+ const outputItemAddedEvent: ResponseStreamEvent = {
+ type: 'response.output_item.added',
+ output_index: 0,
+ item: this.buildOutputMessage(),
+ sequence_number: this.nextSequence()
+ }
+ this.emit(outputItemAddedEvent)
+
+ // Emit content_part.added for text
+ const contentPartAddedEvent: ResponseStreamEvent = {
+ type: 'response.content_part.added',
+ item_id: this.outputItemId,
+ output_index: 0,
+ content_index: this.contentPartIndex,
+ part: {
+ type: 'output_text',
+ text: '',
+ annotations: []
+ },
+ sequence_number: this.nextSequence()
+ }
+ this.emit(contentPartAddedEvent)
+ }
+
+ /**
+ * Build output message object
+ */
+ private buildOutputMessage(): ResponseOutputMessage {
+ return {
+ type: 'message',
+ id: this.outputItemId,
+ status: 'in_progress',
+ role: 'assistant',
+ content: []
+ }
+ }
+
+ /**
+ * Process a single AI SDK chunk and emit corresponding Responses API events
+ */
+ protected processChunk(chunk: TextStreamPart): void {
+ logger.silly('AiSdkToOpenAIResponsesSSE - Processing chunk:', { chunk: JSON.stringify(chunk) })
+
+ switch (chunk.type) {
+ case 'text-delta':
+ this.emitTextDelta(chunk.text || '')
+ break
+
+ case 'tool-call':
+ this.handleToolCall({
+ toolCallId: chunk.toolCallId,
+ toolName: chunk.toolName,
+ args: chunk.input
+ })
+ break
+
+ case 'finish-step':
+ if (chunk.finishReason === 'tool-calls') {
+ this.finishReason = 'tool_calls'
+ }
+ break
+
+ case 'finish':
+ this.handleFinish(chunk)
+ break
+
+ case 'error':
+ throw chunk.error
+
+ default:
+ break
+ }
+ }
+
+ /**
+ * Emit text delta event
+ */
+ private emitTextDelta(delta: string): void {
+ if (!delta) return
+
+ this.textContent += delta
+
+ const event: ResponseStreamEvent = {
+ type: 'response.output_text.delta',
+ item_id: this.outputItemId,
+ output_index: 0,
+ content_index: this.contentPartIndex,
+ delta,
+ logprobs: [],
+ sequence_number: this.nextSequence()
+ }
+ this.emit(event)
+ }
+
+ /**
+ * Handle tool call
+ */
+ private handleToolCall(params: { toolCallId: string; toolName: string; args: unknown }): void {
+ const { toolCallId, toolName, args } = params
+
+ if (this.toolCalls.has(toolCallId)) {
+ return
+ }
+
+ const index = this.currentToolCallIndex++
+ const argsString = JSON.stringify(args)
+
+ this.toolCalls.set(toolCallId, {
+ index,
+ callId: toolCallId,
+ name: toolName,
+ arguments: argsString
+ })
+
+ this.finishReason = 'tool_calls'
+ }
+
+ /**
+ * Handle finish event
+ */
+ private handleFinish(chunk: { type: 'finish'; finishReason?: FinishReason; totalUsage?: LanguageModelUsage }): void {
+ if (chunk.totalUsage) {
+ this.state.inputTokens = chunk.totalUsage.inputTokens || 0
+ this.state.outputTokens = chunk.totalUsage.outputTokens || 0
+ }
+
+ if (!this.finishReason) {
+ switch (chunk.finishReason) {
+ case 'stop':
+ this.finishReason = 'stop'
+ break
+ case 'length':
+ this.finishReason = 'max_output_tokens'
+ break
+ case 'tool-calls':
+ this.finishReason = 'tool_calls'
+ break
+ case 'content-filter':
+ this.finishReason = 'content_filter'
+ break
+ default:
+ this.finishReason = 'stop'
+ }
+ }
+
+ this.state.stopReason = this.finishReason
+ }
+
+ /**
+ * Finalize the stream and emit closing events
+ */
+ protected finalize(): void {
+ // Emit output_text.done
+ const textDoneEvent: ResponseStreamEvent = {
+ type: 'response.output_text.done',
+ item_id: this.outputItemId,
+ output_index: 0,
+ content_index: this.contentPartIndex,
+ text: this.textContent,
+ logprobs: [],
+ sequence_number: this.nextSequence()
+ }
+ this.emit(textDoneEvent)
+
+ // Emit content_part.done
+ const contentPartDoneEvent: ResponseStreamEvent = {
+ type: 'response.content_part.done',
+ item_id: this.outputItemId,
+ output_index: 0,
+ content_index: this.contentPartIndex,
+ part: {
+ type: 'output_text',
+ text: this.textContent,
+ annotations: []
+ },
+ sequence_number: this.nextSequence()
+ }
+ this.emit(contentPartDoneEvent)
+
+ // Emit output_item.done
+ const outputItemDoneEvent: ResponseStreamEvent = {
+ type: 'response.output_item.done',
+ output_index: 0,
+ item: {
+ type: 'message',
+ id: this.outputItemId,
+ status: 'completed',
+ role: 'assistant',
+ content: [
+ {
+ type: 'output_text',
+ text: this.textContent,
+ annotations: []
+ } as ResponseOutputText
+ ]
+ },
+ sequence_number: this.nextSequence()
+ }
+ this.emit(outputItemDoneEvent)
+
+ // Emit response.completed
+ const completedEvent: ResponseStreamEvent = {
+ type: 'response.completed',
+ response: {
+ ...this.buildResponseForEvent('completed'),
+ output: [
+ {
+ type: 'message',
+ id: this.outputItemId,
+ status: 'completed',
+ role: 'assistant',
+ content: [
+ {
+ type: 'output_text',
+ text: this.textContent,
+ annotations: []
+ } as ResponseOutputText
+ ]
+ }
+ ]
+ },
+ sequence_number: this.nextSequence()
+ }
+ this.emit(completedEvent)
+ }
+
+ /**
+ * Build a complete Response object for non-streaming responses.
+ * Returns a partial response cast to Response type.
+ */
+ buildNonStreamingResponse(): Response {
+ const outputText: ResponseOutputText = {
+ type: 'output_text',
+ text: this.textContent,
+ annotations: []
+ }
+
+ const outputMessage: ResponseOutputMessage = {
+ type: 'message',
+ id: this.outputItemId,
+ status: 'completed',
+ role: 'assistant',
+ content: [outputText]
+ }
+
+ const partialResponse: PartialStreamingResponse = {
+ id: `resp_${this.state.messageId}`,
+ object: 'response',
+ created_at: this.createdAt,
+ status: 'completed',
+ model: this.state.model,
+ output: [outputMessage],
+ usage: this.buildUsage()
+ }
+
+ return partialResponse as Response
+ }
+}
+
+export default AiSdkToOpenAIResponsesSSE
diff --git a/src/main/apiServer/app.ts b/src/main/apiServer/app.ts
index 51d4c3dc21..cf6b4d24fc 100644
--- a/src/main/apiServer/app.ts
+++ b/src/main/apiServer/app.ts
@@ -13,6 +13,7 @@ import { chatRoutes } from './routes/chat'
import { mcpRoutes } from './routes/mcp'
import { messagesProviderRoutes, messagesRoutes } from './routes/messages'
import { modelsRoutes } from './routes/models'
+import { responsesRoutes } from './routes/responses'
const logger = loggerService.withContext('ApiServer')
@@ -150,6 +151,7 @@ apiRouter.use('/mcps', mcpRoutes)
apiRouter.use('/messages', extendMessagesTimeout, messagesRoutes)
apiRouter.use('/models', modelsRoutes) // Always enabled
apiRouter.use('/agents', agentsRoutes)
+apiRouter.use('/responses', extendMessagesTimeout, responsesRoutes)
app.use('/v1', apiRouter)
// Error handling (must be last)
diff --git a/src/main/apiServer/routes/responses.ts b/src/main/apiServer/routes/responses.ts
new file mode 100644
index 0000000000..7d1f5c8dfa
--- /dev/null
+++ b/src/main/apiServer/routes/responses.ts
@@ -0,0 +1,306 @@
+import type OpenAI from '@cherrystudio/openai'
+import { isOpenAIProvider } from '@shared/utils/provider'
+import type { Provider } from '@types'
+import type { Request, Response } from 'express'
+import express from 'express'
+
+import { loggerService } from '../../services/LoggerService'
+import type { ResponsesCreateParams } from '../adapters'
+import { processMessage } from '../services/ProxyStreamService'
+import { responsesService } from '../services/responses'
+import { isModelOpenAIResponsesCompatible, validateModelId } from '../utils'
+
+// Use SDK namespace types
+type ResponseCreateParams = OpenAI.Responses.ResponseCreateParams
+
+const logger = loggerService.withContext('ApiServerResponsesRoutes')
+
+const router = express.Router()
+
+/**
+ * Check if provider+model should use direct OpenAI Responses API SDK
+ *
+ * A provider+model combination is considered "OpenAI Responses-compatible" if:
+ * 1. It's a native OpenAI Responses provider (type === 'openai-response'), OR
+ * 2. For aggregated providers (new-api/cherryin), the model has endpoint_type === 'openai-response'
+ *
+ * For these combinations, we bypass AI SDK conversion and use direct passthrough.
+ */
+function shouldUseDirectOpenAIResponses(provider: Provider, modelId: string): boolean {
+ // Native OpenAI Responses provider - always use direct SDK
+ if (isOpenAIProvider(provider)) {
+ return true
+ }
+
+ // Check model-level compatibility for aggregated providers
+ return isModelOpenAIResponsesCompatible(provider, modelId)
+}
+
+interface HandleResponseProcessingOptions {
+ res: Response
+ provider: Provider
+ request: ResponseCreateParams
+ modelId?: string
+}
+
+/**
+ * Handle response processing using direct OpenAI SDK
+ * Used for native OpenAI providers - bypasses AI SDK conversion
+ */
+async function handleDirectOpenAIResponsesProcessing({
+ res,
+ provider,
+ request,
+ modelId
+}: HandleResponseProcessingOptions): Promise {
+ // modelId is guaranteed to be set by caller after validation
+ const actualModelId = modelId!
+
+ logger.info('Processing response via direct OpenAI SDK', {
+ providerId: provider.id,
+ providerType: provider.type,
+ modelId: actualModelId,
+ stream: !!request.stream
+ })
+
+ try {
+ const validation = responsesService.validateRequest(request)
+ if (!validation.isValid) {
+ res.status(400).json({
+ error: {
+ message: validation.errors.join('; '),
+ type: 'invalid_request_error',
+ code: 'validation_error'
+ }
+ })
+ return
+ }
+
+ const { client, openaiRequest } = await responsesService.processResponse({
+ provider,
+ request,
+ modelId: actualModelId
+ })
+
+ if (request.stream) {
+ await responsesService.handleStreaming(client, openaiRequest, { response: res }, provider)
+ } else {
+ const response = await responsesService.handleNonStreaming(client, openaiRequest)
+ res.json(response)
+ }
+ } catch (error: unknown) {
+ logger.error('Direct OpenAI Responses processing error', { error })
+ const { statusCode, errorResponse } = responsesService.transformError(error)
+ res.status(statusCode).json(errorResponse)
+ }
+}
+
+/**
+ * Handle response processing using unified AI SDK
+ * Used for non-OpenAI providers that need format conversion
+ */
+async function handleUnifiedProcessing({
+ res,
+ provider,
+ request,
+ modelId
+}: HandleResponseProcessingOptions): Promise {
+ // modelId is guaranteed to be set by caller after validation
+ const actualModelId = modelId!
+
+ logger.info('Processing response via unified AI SDK', {
+ providerId: provider.id,
+ providerType: provider.type,
+ modelId: actualModelId,
+ stream: !!request.stream
+ })
+
+ try {
+ await processMessage({
+ response: res,
+ provider,
+ modelId: actualModelId,
+ params: request as ResponsesCreateParams,
+ inputFormat: 'openai-responses',
+ outputFormat: 'openai-responses',
+ onError: (error) => {
+ logger.error('Response error', error as Error)
+ },
+ onComplete: () => {
+ logger.debug('Response completed')
+ }
+ })
+ } catch (error: unknown) {
+ logger.error('Unified processing error', { error })
+ const { statusCode, errorResponse } = responsesService.transformError(error)
+ res.status(statusCode).json(errorResponse)
+ }
+}
+
+/**
+ * Handle response processing - routes to appropriate handler based on provider and model
+ *
+ * Routing logic:
+ * - Native OpenAI Responses providers (type === 'openai-response'): Direct OpenAI SDK
+ * - Aggregated providers with model.endpoint_type === 'openai-response': Direct OpenAI SDK
+ * - Other providers/models: Unified AI SDK with Responses API conversion
+ */
+async function handleResponseProcessing(options: HandleResponseProcessingOptions): Promise {
+ const { provider, modelId } = options
+ const actualModelId = modelId!
+
+ if (shouldUseDirectOpenAIResponses(provider, actualModelId)) {
+ return handleDirectOpenAIResponsesProcessing(options)
+ }
+ return handleUnifiedProcessing(options)
+}
+
+/**
+ * @swagger
+ * /v1/responses:
+ * post:
+ * summary: Create a response
+ * description: Create a response using OpenAI Responses API format
+ * tags: [Responses]
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - model
+ * - input
+ * properties:
+ * model:
+ * type: string
+ * description: Model ID in format provider:model or model
+ * input:
+ * oneOf:
+ * - type: string
+ * - type: array
+ * items:
+ * type: object
+ * description: The input to generate a response for
+ * instructions:
+ * type: string
+ * description: System instructions for the model
+ * stream:
+ * type: boolean
+ * description: Whether to stream the response
+ * max_output_tokens:
+ * type: integer
+ * description: Maximum number of output tokens
+ * temperature:
+ * type: number
+ * description: Sampling temperature
+ * tools:
+ * type: array
+ * description: Tools available to the model
+ * responses:
+ * 200:
+ * description: Response created successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * id:
+ * type: string
+ * object:
+ * type: string
+ * example: response
+ * created_at:
+ * type: integer
+ * status:
+ * type: string
+ * model:
+ * type: string
+ * output:
+ * type: array
+ * items:
+ * type: object
+ * usage:
+ * type: object
+ * text/event-stream:
+ * schema:
+ * type: string
+ * description: Server-sent events stream (when stream=true)
+ * 400:
+ * description: Bad request
+ * 401:
+ * description: Unauthorized
+ * 500:
+ * description: Internal server error
+ */
+router.post('/', async (req: Request, res: Response) => {
+ try {
+ const request = req.body as ResponseCreateParams
+
+ if (!request) {
+ return res.status(400).json({
+ error: {
+ message: 'Request body is required',
+ type: 'invalid_request_error',
+ code: 'missing_body'
+ }
+ })
+ }
+
+ if (!request.model) {
+ return res.status(400).json({
+ error: {
+ message: 'Model is required',
+ type: 'invalid_request_error',
+ code: 'missing_model'
+ }
+ })
+ }
+
+ // Responses API uses 'input' instead of 'messages'
+ if (request.input === undefined || request.input === null) {
+ return res.status(400).json({
+ error: {
+ message: 'Input is required',
+ type: 'invalid_request_error',
+ code: 'missing_input'
+ }
+ })
+ }
+
+ logger.debug('Responses API request', {
+ model: request.model,
+ inputType: typeof request.input,
+ stream: request.stream,
+ temperature: request.temperature
+ })
+
+ // Validate model and get provider
+ const modelValidation = await validateModelId(request.model)
+ if (!modelValidation.valid) {
+ return res.status(400).json({
+ error: {
+ message: modelValidation.error?.message || 'Model not found',
+ type: 'invalid_request_error',
+ code: modelValidation.error?.code || 'model_not_found'
+ }
+ })
+ }
+
+ const provider = modelValidation.provider!
+ const modelId = modelValidation.modelId!
+
+ return handleResponseProcessing({
+ res,
+ provider,
+ request,
+ modelId
+ })
+ } catch (error: unknown) {
+ logger.error('Responses API error', { error })
+ const { statusCode, errorResponse } = responsesService.transformError(error)
+ return res.status(statusCode).json(errorResponse)
+ }
+})
+
+export { router as responsesRoutes }
diff --git a/src/main/apiServer/services/responses.ts b/src/main/apiServer/services/responses.ts
new file mode 100644
index 0000000000..f73b8931d8
--- /dev/null
+++ b/src/main/apiServer/services/responses.ts
@@ -0,0 +1,299 @@
+/**
+ * OpenAI Responses API Service
+ *
+ * Provides direct passthrough to OpenAI Responses API for native OpenAI providers,
+ * bypassing AI SDK conversion for optimal performance.
+ *
+ * Similar to MessagesService for Anthropic, this service handles:
+ * - Request validation
+ * - Client creation with proper authentication
+ * - Streaming with SSE event forwarding
+ * - Non-streaming request handling
+ */
+
+import OpenAI from '@cherrystudio/openai'
+import { loggerService } from '@logger'
+import type { Provider } from '@types'
+import { net } from 'electron'
+import type { Response } from 'express'
+
+// Use SDK namespace types
+type ResponseCreateParams = OpenAI.Responses.ResponseCreateParams
+type ResponseStreamEvent = OpenAI.Responses.ResponseStreamEvent
+type ResponseObject = OpenAI.Responses.Response
+
+const logger = loggerService.withContext('ResponsesService')
+
+export interface ValidationResult {
+ isValid: boolean
+ errors: string[]
+}
+
+export interface ErrorResponse {
+ error: {
+ message: string
+ type: string
+ code: string
+ }
+}
+
+export interface StreamConfig {
+ response: Response
+ onChunk?: (chunk: ResponseStreamEvent) => void
+ onError?: (error: unknown) => void
+ onComplete?: () => void
+}
+
+export interface ProcessResponseOptions {
+ provider: Provider
+ request: ResponseCreateParams
+ modelId?: string
+}
+
+export interface ProcessResponseResult {
+ client: OpenAI
+ openaiRequest: ResponseCreateParams
+}
+
+export class ResponsesService {
+ validateRequest(request: ResponseCreateParams): ValidationResult {
+ const errors: string[] = []
+
+ if (!request.model || typeof request.model !== 'string') {
+ errors.push('Model is required')
+ }
+
+ if (request.input === undefined || request.input === null) {
+ errors.push('Input is required')
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ }
+ }
+
+ async getClient(provider: Provider): Promise {
+ // Create OpenAI client with Electron's net.fetch
+ const electronFetch: typeof globalThis.fetch = async (input: URL | RequestInfo, init?: RequestInit) => {
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
+ if (init) {
+ const initWithAgent = init as RequestInit & { agent?: unknown }
+ delete initWithAgent.agent
+ const headers = new Headers(initWithAgent.headers)
+ if (headers.has('content-length')) {
+ headers.delete('content-length')
+ }
+ initWithAgent.headers = headers
+ return net.fetch(url, initWithAgent)
+ }
+ return net.fetch(url)
+ }
+
+ const baseURL = provider.apiHost?.replace(/\/$/, '')
+
+ return new OpenAI({
+ apiKey: provider.apiKey || '',
+ baseURL,
+ fetch: electronFetch
+ })
+ }
+
+ createOpenAIRequest(request: ResponseCreateParams, modelId?: string): ResponseCreateParams {
+ const openaiRequest: ResponseCreateParams = {
+ ...request
+ }
+
+ if (modelId) {
+ openaiRequest.model = modelId
+ }
+
+ return openaiRequest
+ }
+
+ async handleStreaming(
+ client: OpenAI,
+ request: ResponseCreateParams,
+ config: StreamConfig,
+ provider: Provider
+ ): Promise {
+ const { response, onChunk, onError, onComplete } = config
+
+ // Set streaming headers
+ response.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
+ response.setHeader('Cache-Control', 'no-cache, no-transform')
+ response.setHeader('Connection', 'keep-alive')
+ response.setHeader('X-Accel-Buffering', 'no')
+ response.flushHeaders()
+
+ const flushableResponse = response as Response & { flush?: () => void }
+ const flushStream = () => {
+ if (typeof flushableResponse.flush !== 'function') {
+ return
+ }
+ try {
+ flushableResponse.flush()
+ } catch (flushError: unknown) {
+ logger.warn('Failed to flush streaming response', { error: flushError })
+ }
+ }
+
+ const writeSse = (event: ResponseStreamEvent) => {
+ if (response.writableEnded || response.destroyed) {
+ return
+ }
+
+ // Responses API uses named events
+ response.write(`event: ${event.type}\n`)
+ response.write(`data: ${JSON.stringify(event)}\n\n`)
+ flushStream()
+ }
+
+ try {
+ // Use stream: true to get Stream
+ const stream = await client.responses.create({
+ ...request,
+ stream: true
+ } as OpenAI.Responses.ResponseCreateParamsStreaming)
+
+ for await (const chunk of stream) {
+ if (response.writableEnded || response.destroyed) {
+ logger.warn('Streaming response ended before stream completion', {
+ provider: provider.id,
+ model: request.model
+ })
+ break
+ }
+
+ writeSse(chunk)
+
+ if (onChunk) {
+ onChunk(chunk)
+ }
+ }
+
+ // Send done marker
+ if (!response.writableEnded) {
+ response.write('data: [DONE]\n\n')
+ flushStream()
+ }
+
+ if (onComplete) {
+ onComplete()
+ }
+ } catch (streamError: unknown) {
+ logger.error('Stream error', {
+ error: streamError,
+ provider: provider.id,
+ model: request.model
+ })
+
+ if (!response.writableEnded) {
+ const errorEvent = {
+ type: 'error',
+ error: {
+ message: streamError instanceof Error ? streamError.message : 'Stream processing error',
+ type: 'api_error',
+ code: 'stream_error'
+ }
+ }
+ response.write(`event: error\n`)
+ response.write(`data: ${JSON.stringify(errorEvent)}\n\n`)
+ flushStream()
+ }
+
+ if (onError) {
+ onError(streamError)
+ }
+ } finally {
+ if (!response.writableEnded) {
+ response.end()
+ }
+ }
+ }
+
+ async handleNonStreaming(client: OpenAI, request: ResponseCreateParams): Promise {
+ return client.responses.create({
+ ...request,
+ stream: false
+ } as OpenAI.Responses.ResponseCreateParamsNonStreaming)
+ }
+
+ transformError(error: unknown): { statusCode: number; errorResponse: ErrorResponse } {
+ let statusCode = 500
+ let errorType = 'server_error'
+ let errorCode = 'internal_error'
+ let errorMessage = 'Internal server error'
+
+ if (error instanceof OpenAI.APIError) {
+ statusCode = error.status || 500
+ errorMessage = error.message
+
+ if (statusCode === 400) {
+ errorType = 'invalid_request_error'
+ errorCode = 'bad_request'
+ } else if (statusCode === 401) {
+ errorType = 'authentication_error'
+ errorCode = 'invalid_api_key'
+ } else if (statusCode === 403) {
+ errorType = 'forbidden_error'
+ errorCode = 'forbidden'
+ } else if (statusCode === 404) {
+ errorType = 'not_found_error'
+ errorCode = 'not_found'
+ } else if (statusCode === 429) {
+ errorType = 'rate_limit_error'
+ errorCode = 'rate_limit_exceeded'
+ }
+ } else if (error instanceof Error) {
+ errorMessage = error.message
+
+ if (errorMessage.includes('API key') || errorMessage.includes('authentication')) {
+ statusCode = 401
+ errorType = 'authentication_error'
+ errorCode = 'invalid_api_key'
+ } else if (errorMessage.includes('rate limit') || errorMessage.includes('quota')) {
+ statusCode = 429
+ errorType = 'rate_limit_error'
+ errorCode = 'rate_limit_exceeded'
+ } else if (errorMessage.includes('timeout') || errorMessage.includes('connection')) {
+ statusCode = 502
+ errorType = 'server_error'
+ errorCode = 'upstream_error'
+ }
+ }
+
+ return {
+ statusCode,
+ errorResponse: {
+ error: {
+ message: errorMessage,
+ type: errorType,
+ code: errorCode
+ }
+ }
+ }
+ }
+
+ async processResponse(options: ProcessResponseOptions): Promise {
+ const { provider, request, modelId } = options
+
+ const client = await this.getClient(provider)
+ const openaiRequest = this.createOpenAIRequest(request, modelId)
+
+ logger.info('Processing OpenAI Responses API request', {
+ provider: provider.id,
+ apiHost: provider.apiHost,
+ model: openaiRequest.model,
+ stream: !!request.stream,
+ inputType: typeof request.input
+ })
+
+ return {
+ client,
+ openaiRequest
+ }
+ }
+}
+
+export const responsesService = new ResponsesService()
diff --git a/src/main/apiServer/utils/index.ts b/src/main/apiServer/utils/index.ts
index df27269178..5862ba9604 100644
--- a/src/main/apiServer/utils/index.ts
+++ b/src/main/apiServer/utils/index.ts
@@ -1,6 +1,7 @@
import { CacheService } from '@main/services/CacheService'
import { loggerService } from '@main/services/LoggerService'
import { reduxService } from '@main/services/ReduxService'
+import { isOpenAILLMModel } from '@shared/aiCore/config/aihubmix'
import { isPpioAnthropicCompatibleModel, isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
import type { ApiModel, Model, Provider } from '@types'
@@ -274,6 +275,7 @@ export function validateProvider(provider: Provider): boolean {
}
}
+// TODO: checker 和 ProviderCreator重构到一起
export const getProviderAnthropicModelChecker = (providerId: string): ((m: Model) => boolean) => {
switch (providerId) {
case 'cherryin':
@@ -293,6 +295,25 @@ export const getProviderAnthropicModelChecker = (providerId: string): ((m: Model
}
}
+/**
+ * Get model checker for OpenAI Responses API compatibility
+ *
+ * Returns a function that checks if a model supports OpenAI Responses API endpoint.
+ * For aggregated providers, only certain models may support this endpoint.
+ */
+export const getProviderOpenAIResponsesModelChecker = (providerId: string): ((m: Model) => boolean) => {
+ switch (providerId) {
+ case 'cherryin':
+ case 'new-api':
+ // Check endpoint_type for responses API support
+ return (m: Model) => m.endpoint_type === 'openai-response'
+ case 'aihubmix':
+ return (m) => isOpenAILLMModel(m)
+ default:
+ return () => false
+ }
+}
+
/**
* Check if a specific model is compatible with Anthropic API for a given provider.
*
@@ -321,3 +342,32 @@ export function isModelAnthropicCompatible(provider: Provider, modelId: string):
return checker(minimalModel)
}
+
+/**
+ * Check if a specific model is compatible with OpenAI Responses API for a given provider.
+ *
+ * This is used for fine-grained routing decisions at the model level.
+ * For aggregated providers (like new-api/cherryin), only certain models support the Responses API endpoint.
+ *
+ * @param provider - The provider to check
+ * @param modelId - The model ID to check (without provider prefix)
+ * @returns true if the model supports OpenAI Responses API endpoint
+ */
+export function isModelOpenAIResponsesCompatible(provider: Provider, modelId: string): boolean {
+ const checker = getProviderOpenAIResponsesModelChecker(provider.id)
+
+ const model = provider.models?.find((m) => m.id === modelId)
+
+ if (model) {
+ return checker(model)
+ }
+
+ const minimalModel: Model = {
+ id: modelId,
+ name: modelId,
+ provider: provider.id,
+ group: ''
+ }
+
+ return checker(minimalModel)
+}