From 496d7797e5f1a4fd7ea2051d3b79303911a3c421 Mon Sep 17 00:00:00 2001 From: lxxxxxx Date: Tue, 28 Apr 2026 19:34:17 +0800 Subject: [PATCH] feat(config): support 'openai-responses' as customProtocol value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repository already supports the OpenAI Responses API natively in pi-coding-agent (api: 'openai-responses'), but there's no way for a user to opt in to it via a custom relay — every custom-provider Responses-API model gets force-downgraded to openai-completions, on the assumption that 'most custom OpenAI-compatible relays only implement chat/completions'. That's a reasonable default, but it leaves no escape hatch for users whose relay does implement /v1/responses. Add 'openai-responses' as a fourth customProtocol value (alongside 'anthropic' / 'openai' / 'gemini'): - pi-model-resolution.ts: - resolvePiRouteProtocol returns 'openai-responses' when the user picks it explicitly. - inferPiApi maps 'openai-responses' → 'openai-responses'. - applyPiModelRuntimeOverrides only force-downgrades to openai-completions when customProtocol !== 'openai-responses'. When the user explicitly opted in, force the api to 'openai-responses' (so the relay receives /v1/responses calls). - config-store.ts: - CustomProtocolType union adds 'openai-responses'. - isCustomProtocol type guard accepts it. - profileKeyFromProvider routes 'openai-responses' to the same 'custom:openai' profile slot — both protocols share apiKey/baseUrl, so no need to force the user to re-enter creds when toggling. - renderer/types/index.ts + shared/api-model-presets.ts: mirror the type union so the renderer/shared types accept the new value. Behavior preserved for existing users: anyone with customProtocol set to 'openai' still gets the chat/completions downgrade. Only users who explicitly switch to 'openai-responses' get the new behavior. --- src/main/claude/pi-model-resolution.ts | 28 +++++++++++++++++++++++--- src/main/config/config-store.ts | 14 ++++++++++--- src/renderer/types/index.ts | 2 +- src/shared/api-model-presets.ts | 2 +- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/main/claude/pi-model-resolution.ts b/src/main/claude/pi-model-resolution.ts index 15fba88e..c283ef45 100644 --- a/src/main/claude/pi-model-resolution.ts +++ b/src/main/claude/pi-model-resolution.ts @@ -41,7 +41,11 @@ export interface SyntheticPiModelFallback { export function resolvePiRouteProtocol(provider?: string, customProtocol?: string): string { if (provider === 'custom') { - if (customProtocol === 'openai' || customProtocol === 'gemini') { + if ( + customProtocol === 'openai' || + customProtocol === 'openai-responses' || + customProtocol === 'gemini' + ) { return customProtocol; } return 'anthropic'; @@ -76,6 +80,8 @@ export function inferPiApi(protocol: string): string { case 'gemini': case 'google': return 'google-generative-ai'; + case 'openai-responses': + return 'openai-responses'; case 'openai': default: return 'openai-completions'; @@ -267,10 +273,26 @@ export function applyPiModelRuntimeOverrides( } const effectiveProvider = options.rawProvider || options.configProvider; - if (options.customBaseUrl && isCustomProvider && nextModel.api === 'openai-responses') { - // Most custom OpenAI-compatible relays only implement chat/completions. + if ( + options.customBaseUrl && + isCustomProvider && + nextModel.api === 'openai-responses' && + options.customProtocol !== 'openai-responses' + ) { + // Most custom OpenAI-compatible relays only implement chat/completions, so + // downgrade by default. Skip the downgrade when the user has explicitly + // chosen 'openai-responses' as the customProtocol — that's an opt-in signal + // that their relay implements the Responses API. nextModel = { ...nextModel, api: 'openai-completions' } as typeof nextModel; } + if ( + isCustomProvider && + options.customProtocol === 'openai-responses' && + nextModel.api !== 'openai-responses' + ) { + // User explicitly opted in to Responses API for this custom relay. + nextModel = { ...nextModel, api: 'openai-responses' } as typeof nextModel; + } if (effectiveProvider === 'openrouter' && nextModel.api !== 'openai-completions') { nextModel = { ...nextModel, api: 'openai-completions' } as typeof nextModel; } diff --git a/src/main/config/config-store.ts b/src/main/config/config-store.ts index 6308fa44..f7983636 100644 --- a/src/main/config/config-store.ts +++ b/src/main/config/config-store.ts @@ -35,7 +35,7 @@ import { API_PROVIDER_PRESETS, PI_AI_CURATED_PRESETS } from '../../shared/api-mo * Application configuration schema */ export type ProviderType = 'openrouter' | 'anthropic' | 'custom' | 'openai' | 'gemini' | 'ollama'; -export type CustomProtocolType = 'anthropic' | 'openai' | 'gemini'; +export type CustomProtocolType = 'anthropic' | 'openai' | 'openai-responses' | 'gemini'; export type AppTheme = 'dark' | 'light' | 'system'; export type ProviderProfileKey = | 'openrouter' @@ -293,7 +293,12 @@ function isProviderType(value: unknown): value is ProviderType { } function isCustomProtocol(value: unknown): value is CustomProtocolType { - return value === 'anthropic' || value === 'openai' || value === 'gemini'; + return ( + value === 'anthropic' || + value === 'openai' || + value === 'openai-responses' || + value === 'gemini' + ); } function isProfileKey(value: unknown): value is ProviderProfileKey { @@ -311,7 +316,10 @@ function profileKeyFromProvider( if (provider !== 'custom') { return provider; } - if (customProtocol === 'openai') { + if (customProtocol === 'openai' || customProtocol === 'openai-responses') { + // openai-responses shares the profile slot with openai (same baseUrl/key, + // different request shape). Avoids forcing the user to re-enter creds when + // toggling between the two protocols on the same relay. return 'custom:openai'; } if (customProtocol === 'gemini') { diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 06f5cadb..04ec9e76 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -428,7 +428,7 @@ export interface ExecutionContext { // App Config types export type ProviderType = 'openrouter' | 'anthropic' | 'custom' | 'openai' | 'gemini' | 'ollama'; -export type CustomProtocolType = 'anthropic' | 'openai' | 'gemini'; +export type CustomProtocolType = 'anthropic' | 'openai' | 'openai-responses' | 'gemini'; export type AppTheme = 'dark' | 'light' | 'system'; export type ProviderProfileKey = | 'openrouter' diff --git a/src/shared/api-model-presets.ts b/src/shared/api-model-presets.ts index 2ac15700..733b5bfa 100644 --- a/src/shared/api-model-presets.ts +++ b/src/shared/api-model-presets.ts @@ -6,7 +6,7 @@ export type SharedProviderType = | 'gemini' | 'ollama'; -export type SharedCustomProtocolType = 'anthropic' | 'openai' | 'gemini'; +export type SharedCustomProtocolType = 'anthropic' | 'openai' | 'openai-responses' | 'gemini'; export interface SharedProviderPreset { name: string;