@@ -19,6 +19,30 @@ export interface OpenAIAdapterConfig {
1919 fetchImpl ?: typeof fetch ;
2020}
2121
22+ /**
23+ * OpenAI's "pro" reasoning models (gpt-5-pro, gpt-5.5-pro, o1-pro, o3-pro) and
24+ * the codex models are served only by the Responses API (/v1/responses). Posting
25+ * them to /chat/completions 404s with "This is not a chat model...". Route those
26+ * to /responses. Only OpenAI itself exposes that endpoint — the other
27+ * OpenAI-compatible providers (DeepSeek, Perplexity, …) stay on /chat/completions.
28+ */
29+ export function usesResponsesApi ( providerId : ProviderId , model : string ) : boolean {
30+ return providerId === 'openai' && / - p r o ( \b | - ) | c o d e x / i. test ( model ) ;
31+ }
32+
33+ /** Concatenates the text of every `output_text` part in a Responses `output`. */
34+ function extractResponsesText (
35+ output ?: { type ?: string ; content ?: { type ?: string ; text ?: string } [ ] } [ ] ,
36+ ) : string {
37+ let text = '' ;
38+ for ( const item of output ?? [ ] ) {
39+ for ( const part of item . content ?? [ ] ) {
40+ if ( part . type === 'output_text' && part . text ) text += part . text ;
41+ }
42+ }
43+ return text ;
44+ }
45+
2246export class OpenAICompatibleProvider implements ModelProvider {
2347 constructor (
2448 readonly id : ProviderId ,
@@ -44,6 +68,7 @@ export class OpenAICompatibleProvider implements ModelProvider {
4468 }
4569
4670 async complete ( req : CompletionRequest ) : Promise < CompletionResponse > {
71+ if ( usesResponsesApi ( this . id , req . model ) ) return this . completeViaResponses ( req ) ;
4772 const res = await this . fetch ( `${ this . config . baseUrl } /chat/completions` , {
4873 method : 'POST' ,
4974 headers : this . headers ( ) ,
@@ -72,6 +97,10 @@ export class OpenAICompatibleProvider implements ModelProvider {
7297 }
7398
7499 async * stream ( req : CompletionRequest ) : AsyncIterable < CompletionChunk > {
100+ if ( usesResponsesApi ( this . id , req . model ) ) {
101+ yield * this . streamViaResponses ( req ) ;
102+ return ;
103+ }
75104 const res = await this . fetch ( `${ this . config . baseUrl } /chat/completions` , {
76105 method : 'POST' ,
77106 headers : this . headers ( ) ,
@@ -101,6 +130,73 @@ export class OpenAICompatibleProvider implements ModelProvider {
101130 }
102131 yield { delta : '' , done : true } ;
103132 }
133+
134+ /**
135+ * Responses API (/v1/responses) path for OpenAI's pro/codex models. The
136+ * role/content messages map straight onto `input`; pro models reject
137+ * `temperature`, so it is omitted.
138+ */
139+ private async completeViaResponses ( req : CompletionRequest ) : Promise < CompletionResponse > {
140+ const res = await this . fetch ( `${ this . config . baseUrl } /responses` , {
141+ method : 'POST' ,
142+ headers : this . headers ( ) ,
143+ body : JSON . stringify ( {
144+ model : req . model ,
145+ input : req . messages ,
146+ max_output_tokens : req . maxTokens ,
147+ stream : false ,
148+ } ) ,
149+ } ) ;
150+ if ( ! res . ok ) throw new Error ( `${ this . id } completion failed: ${ res . status } ${ await res . text ( ) } ` ) ;
151+ const body = ( await res . json ( ) ) as {
152+ output_text ?: string ;
153+ output ?: { type ?: string ; content ?: { type ?: string ; text ?: string } [ ] } [ ] ;
154+ usage ?: { input_tokens : number ; output_tokens : number } ;
155+ } ;
156+ const text = body . output_text ?? extractResponsesText ( body . output ) ;
157+ const out : CompletionResponse = { text, model : req . model } ;
158+ if ( body . usage ) {
159+ out . usage = {
160+ promptTokens : body . usage . input_tokens ,
161+ completionTokens : body . usage . output_tokens ,
162+ } ;
163+ }
164+ return out ;
165+ }
166+
167+ private async * streamViaResponses ( req : CompletionRequest ) : AsyncIterable < CompletionChunk > {
168+ const res = await this . fetch ( `${ this . config . baseUrl } /responses` , {
169+ method : 'POST' ,
170+ headers : this . headers ( ) ,
171+ body : JSON . stringify ( {
172+ model : req . model ,
173+ input : req . messages ,
174+ max_output_tokens : req . maxTokens ,
175+ stream : true ,
176+ } ) ,
177+ } ) ;
178+ if ( ! res . ok || ! res . body ) {
179+ throw new Error ( `${ this . id } stream failed: ${ res . status } ` ) ;
180+ }
181+ for await ( const data of sseLines ( res . body ) ) {
182+ if ( data === '[DONE]' ) {
183+ yield { delta : '' , done : true } ;
184+ return ;
185+ }
186+ try {
187+ const evt = JSON . parse ( data ) as { type ?: string ; delta ?: string } ;
188+ if ( evt . type === 'response.output_text.delta' && typeof evt . delta === 'string' ) {
189+ yield { delta : evt . delta , done : false } ;
190+ } else if ( evt . type === 'response.completed' || evt . type === 'response.failed' ) {
191+ yield { delta : '' , done : true } ;
192+ return ;
193+ }
194+ } catch {
195+ // ignore keep-alive / non-JSON lines
196+ }
197+ }
198+ yield { delta : '' , done : true } ;
199+ }
104200}
105201
106202/** Yields the payload of each `data:` line from an SSE response stream. */
0 commit comments