@@ -126,6 +126,31 @@ function resolveReferrer() {
126126 return DEFAULT_REFERRER ;
127127}
128128
129+ function shouldRetryWithoutJson ( error ) {
130+ if ( ! error ) return false ;
131+ const status = typeof error . status === 'number' ? error . status : null ;
132+ if ( status === 429 ) return false ;
133+ if ( status && Number . isFinite ( status ) ) {
134+ if ( status >= 500 ) return true ;
135+ if ( [ 400 , 408 , 409 , 413 , 415 , 422 ] . includes ( status ) ) return true ;
136+ }
137+ const message = String ( error ?. message || '' ) . toLowerCase ( ) ;
138+ if ( ! message ) return false ;
139+ if ( / h t t p \s + 4 2 9 / . test ( message ) ) return false ;
140+ const match = / h t t p \s + ( \d { 3 } ) / . exec ( message ) ;
141+ if ( match ) {
142+ const code = Number ( match [ 1 ] ) ;
143+ if ( Number . isFinite ( code ) ) {
144+ if ( code >= 500 ) return true ;
145+ if ( [ 400 , 408 , 409 , 413 , 415 , 422 ] . includes ( code ) ) return true ;
146+ }
147+ }
148+ if ( message . includes ( 'json' ) ) return true ;
149+ if ( message . includes ( 'schema' ) ) return true ;
150+ if ( message . includes ( 'response_format' ) ) return true ;
151+ return false ;
152+ }
153+
129154export async function textModels ( client ) {
130155 const c = client instanceof PolliClient ? client : new PolliClient ( ) ;
131156 return c . listModels ( 'text' ) ;
@@ -135,51 +160,89 @@ export async function chat(payload, client) {
135160 const c = client instanceof PolliClient ? client : new PolliClient ( ) ;
136161 const referrer = resolveReferrer ( ) ;
137162 const { endpoint = 'openai' , model : selectedModel = 'openai' , messages = [ ] , tools = null , tool_choice = 'auto' , ...extra } = payload || { } ;
163+ const { response_format : providedResponseFormat , jsonMode, ...rest } = extra || { } ;
164+ const responseFormat = providedResponseFormat || ( jsonMode ? { type : 'json_object' } : null ) ;
138165
139166 const url = `${ c . textPromptBase } /openai` ;
140167 const filteredMessages = Array . isArray ( messages ) ? messages . filter ( m => ! m || typeof m !== 'object' || m . role !== 'system' ) : [ ] ;
141- const body = {
168+ const baseBody = {
142169 model : selectedModel ,
143170 messages : filteredMessages ,
144171 ...( referrer ? { referrer } : { } ) ,
145- ...( extra . seed != null ? { seed : extra . seed } : { } ) ,
172+ ...( rest . seed != null ? { seed : rest . seed } : { } ) ,
146173 ...( Array . isArray ( tools ) && tools . length ? { tools, tool_choice } : { } ) ,
147- ...( extra . response_format ? { response_format : extra . response_format } : ( extra . jsonMode ? { response_format : { type : 'json_object' } } : { } ) ) ,
174+ ...rest ,
148175 } ;
149176
150177 const controller = new AbortController ( ) ;
151178 const t = setTimeout ( ( ) => controller . abort ( ) , c . timeoutMs ) ;
179+ const wantsJson = ! ! responseFormat ;
180+ const attemptModes = wantsJson ? [ true , false ] : [ false ] ;
181+ let fallbackUsed = false ;
182+ let lastError = null ;
152183 try {
153- try {
154- let log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
155- if ( ! log && globalThis ) { globalThis . __PANEL_LOG__ = [ ] ; log = globalThis . __PANEL_LOG__ ; }
156- if ( log && Array . isArray ( log ) ) {
157- log . push ( { ts : Date . now ( ) , kind : 'chat:request' , url, model : selectedModel , referer : referrer || null , meta : { tool_count : Array . isArray ( tools ) ? tools . length : 0 , endpoint : endpoint || 'openai' , json : ! ! extra ?. response_format } } ) ;
184+ for ( const useJson of attemptModes ) {
185+ const attemptBody = { ...baseBody } ;
186+ if ( useJson && responseFormat ) {
187+ attemptBody . response_format = responseFormat ;
188+ } else {
189+ delete attemptBody . response_format ;
158190 }
159- } catch { }
160- const t0 = Date . now ( ) ;
161- const r = await c . fetch ( url , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) , signal : controller . signal } ) ;
162- const ms = Date . now ( ) - t0 ;
163- if ( ! r . ok ) {
164- try { const log = ( globalThis && globalThis . __PANEL_LOG__ ) ; if ( log && Array . isArray ( log ) ) log . push ( { ts : Date . now ( ) , kind : 'chat:error' , url, model : selectedModel , ok : false , status : r . status , ms } ) ; } catch { }
165- throw new Error ( `HTTP ${ r . status } ` ) ;
166- }
167- const data = await r . json ( ) ;
168- try {
169- if ( data && typeof data === 'object' ) {
170- const meta = data . metadata && typeof data . metadata === 'object' ? data . metadata : ( data . metadata = { } ) ;
171- meta . requested_model = selectedModel ;
172- meta . requestedModel = selectedModel ;
173- meta . endpoint = endpoint || 'openai' ;
174- if ( ! Array . isArray ( data . modelAliases ) ) data . modelAliases = [ ] ;
175- if ( ! data . modelAliases . includes ( selectedModel ) ) data . modelAliases . push ( selectedModel ) ;
191+ try {
192+ try {
193+ let log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
194+ if ( ! log && globalThis ) { globalThis . __PANEL_LOG__ = [ ] ; log = globalThis . __PANEL_LOG__ ; }
195+ if ( log && Array . isArray ( log ) ) {
196+ log . push ( { ts : Date . now ( ) , kind : 'chat:request' , url, model : selectedModel , referer : referrer || null , meta : { tool_count : Array . isArray ( tools ) ? tools . length : 0 , endpoint : endpoint || 'openai' , json : useJson } } ) ;
197+ }
198+ } catch { }
199+ const t0 = Date . now ( ) ;
200+ const r = await c . fetch ( url , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( attemptBody ) , signal : controller . signal } ) ;
201+ const ms = Date . now ( ) - t0 ;
202+ if ( ! r . ok ) {
203+ try {
204+ const log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
205+ if ( log && Array . isArray ( log ) ) log . push ( { ts : Date . now ( ) , kind : 'chat:error' , url, model : selectedModel , ok : false , status : r . status , ms, meta : { json : useJson } } ) ;
206+ } catch { }
207+ const err = new Error ( `HTTP ${ r . status } ` ) ;
208+ err . status = r . status ;
209+ err . statusText = r . statusText ;
210+ throw err ;
211+ }
212+ const data = await r . json ( ) ;
213+ try {
214+ if ( data && typeof data === 'object' ) {
215+ const meta = data . metadata && typeof data . metadata === 'object' ? data . metadata : ( data . metadata = { } ) ;
216+ meta . requested_model = selectedModel ;
217+ meta . requestedModel = selectedModel ;
218+ meta . endpoint = endpoint || 'openai' ;
219+ meta . response_format_requested = wantsJson ;
220+ meta . response_format_used = ! ! ( useJson && responseFormat ) ;
221+ meta . jsonFallbackUsed = ! ! fallbackUsed ;
222+ if ( ! Array . isArray ( data . modelAliases ) ) data . modelAliases = [ ] ;
223+ if ( ! data . modelAliases . includes ( selectedModel ) ) data . modelAliases . push ( selectedModel ) ;
224+ }
225+ } catch { }
226+ try {
227+ const log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
228+ if ( log && Array . isArray ( log ) ) log . push ( { ts : Date . now ( ) , kind : 'chat:response' , url, model : data ?. model || null , ok : true , ms, meta : { json : useJson , fallback : fallbackUsed } } ) ;
229+ } catch { }
230+ return data ;
231+ } catch ( error ) {
232+ lastError = error ;
233+ if ( useJson && wantsJson && ! fallbackUsed && shouldRetryWithoutJson ( error ) ) {
234+ fallbackUsed = true ;
235+ try {
236+ const log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
237+ if ( log && Array . isArray ( log ) ) log . push ( { ts : Date . now ( ) , kind : 'chat:retry' , url, model : selectedModel , meta : { reason : 'json_fallback' } } ) ;
238+ } catch { }
239+ continue ;
240+ }
241+ throw error ;
176242 }
177- } catch { }
178- try {
179- const log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
180- if ( log && Array . isArray ( log ) ) log . push ( { ts : Date . now ( ) , kind : 'chat:response' , url, model : data ?. model || null , ok : true , ms } ) ;
181- } catch { }
182- return data ;
243+ }
244+ if ( lastError ) throw lastError ;
245+ throw new Error ( 'Chat request failed without response.' ) ;
183246 } finally {
184247 try {
185248 const log = ( globalThis && globalThis . __PANEL_LOG__ ) ;
0 commit comments