@@ -103,7 +103,10 @@ function buildUpstreamPool(readConfigFn, openaiBridgeFile, excludedProviders) {
103103 const baseUrl = typeof p . base_url === 'string' ? p . base_url . trim ( ) : '' ;
104104 if ( ! isValidHttpUrl ( normalizeBaseUrl ( baseUrl ) ) ) continue ;
105105 const authMethod = typeof p . preferred_auth_method === 'string' ? p . preferred_auth_method . trim ( ) : '' ;
106- pool . push ( { name, baseUrl : normalizeBaseUrl ( baseUrl ) , authMethod, requiresOpenaiAuth : ! ! p . requires_openai_auth } ) ;
106+ const models = Array . isArray ( p . models ) ? p . models . filter ( item => typeof item === 'string' && item . trim ( ) ) . map ( item => item . trim ( ) ) : [ ] ;
107+ const model = typeof p . selected_model === 'string' && p . selected_model . trim ( ) ? p . selected_model . trim ( ) : ( models [ 0 ] || '' ) ;
108+ const temperature = typeof p . temperature === 'number' && Number . isFinite ( p . temperature ) ? p . temperature : null ;
109+ pool . push ( { name, baseUrl : normalizeBaseUrl ( baseUrl ) , authMethod, requiresOpenaiAuth : ! ! p . requires_openai_auth , model, temperature } ) ;
107110 }
108111 if ( pool . length === 0 ) return { error : '请先添加上游 provider' } ;
109112 return { pool } ;
@@ -151,8 +154,11 @@ function resolveUpstreamAuth(entry, openaiBridgeFile, reqAuthToken) {
151154 if ( upstream && upstream . apiKey ) {
152155 return upstream . apiKey . startsWith ( 'Bearer ' ) ? upstream . apiKey : `Bearer ${ upstream . apiKey } ` ;
153156 }
157+ return '' ;
154158 }
155- return '' ;
159+ const providerToken = typeof entry . authMethod === 'string' ? entry . authMethod . trim ( ) : '' ;
160+ if ( ! providerToken ) return '' ;
161+ return / ^ B e a r e r \s + / i. test ( providerToken ) ? providerToken : `Bearer ${ providerToken } ` ;
156162}
157163
158164function createLocalBridgeHttpHandler ( options = { } ) {
@@ -186,6 +192,20 @@ function createLocalBridgeHttpHandler(options = {}) {
186192 return { entry : pool [ 0 ] , idx : 0 } ;
187193 }
188194
195+ function pickUpstreamExcluding ( pool , excludedNames ) {
196+ const now = Date . now ( ) ;
197+ const excluded = excludedNames instanceof Set ? excludedNames : new Set ( ) ;
198+ for ( let i = 0 ; i < pool . length ; i ++ ) {
199+ const idx = rrIndex ++ % pool . length ;
200+ const entry = pool [ idx ] ;
201+ if ( excluded . has ( entry . name ) ) continue ;
202+ const st = circuitState . get ( entry . name ) ;
203+ if ( st && st . openUntil > now ) continue ;
204+ return { entry, idx } ;
205+ }
206+ return null ;
207+ }
208+
189209 function recordFailure ( name ) {
190210 let st = circuitState . get ( name ) ;
191211 if ( ! st ) { st = { failures : 0 , openUntil : 0 } ; circuitState . set ( name , st ) ; }
@@ -722,25 +742,98 @@ function createLocalBridgeHttpHandler(options = {}) {
722742 return ;
723743 }
724744
725- // passthrough for other v1/* paths
726- const upstreamUrl = joinApiUrl ( upstreamBase , normalizedSuffix ) ;
727- const upstreamResult = await retryTransientRequest ( ( ) => proxyRequestJson ( upstreamUrl , {
728- method : req . method || 'GET' ,
729- body : null ,
730- headers : { ...( authHeader ? { Authorization : authHeader } : { } ) } ,
731- maxBytes : maxUpstreamBytes ,
732- httpAgent,
733- httpsAgent
734- } ) ) ;
735- if ( ! upstreamResult . ok ) {
736- recordFailure ( entry . name ) ;
737- res . writeHead ( 502 , { 'Content-Type' : 'application/json; charset=utf-8' } ) ;
738- res . end ( JSON . stringify ( { error : `Upstream request failed: ${ upstreamResult . error } ` } ) ) ;
745+ // OpenAI-compatible passthrough for /bridge/local/v1/* paths.
746+ // Automatic policy: retry transient errors, fail over to the next provider,
747+ // and let the circuit breaker freeze repeatedly failing providers.
748+ const method = ( req . method || 'GET' ) . toUpperCase ( ) ;
749+ let passthroughBody = null ;
750+ if ( method !== 'GET' && method !== 'HEAD' ) {
751+ const bodyResult = await readRequestBody ( req , maxBodySize ) ;
752+ if ( bodyResult . error ) {
753+ res . writeHead ( 413 , { 'Content-Type' : 'application/json; charset=utf-8' } ) ;
754+ res . end ( JSON . stringify ( { error : bodyResult . error } ) ) ;
755+ return ;
756+ }
757+ if ( String ( bodyResult . body || '' ) . trim ( ) ) {
758+ const parsedBody = parseJsonOrError ( bodyResult . body ) ;
759+ if ( parsedBody . error ) {
760+ res . writeHead ( 400 , { 'Content-Type' : 'application/json; charset=utf-8' } ) ;
761+ res . end ( JSON . stringify ( { error : `Invalid JSON: ${ parsedBody . error } ` } ) ) ;
762+ return ;
763+ }
764+ passthroughBody = parsedBody . value ;
765+ }
766+ }
767+
768+ const attemptedProviders = [ ] ;
769+ const attemptedProviderNames = new Set ( ) ;
770+ let lastResult = null ;
771+ let lastEntry = null ;
772+ const shouldFailover = ( result ) => {
773+ if ( ! result || ! result . ok ) return true ;
774+ const status = Number ( result . status || 0 ) ;
775+ return status === 401 || status === 403 || status === 429 || status >= 500 ;
776+ } ;
777+
778+ for ( let attempt = 0 ; attempt < pool . length ; attempt += 1 ) {
779+ const selected = attempt === 0 ? { entry, idx } : pickUpstreamExcluding ( pool , attemptedProviderNames ) ;
780+ if ( ! selected || ! selected . entry ) break ;
781+ const attemptEntry = selected . entry ;
782+ const attemptAuthHeader = resolveUpstreamAuth ( attemptEntry , openaiBridgeFile , token ) ;
783+ const attemptBase = attemptEntry . baseUrl . replace ( / \/ + $ / , '' ) ;
784+ const upstreamUrl = joinApiUrl ( attemptBase , normalizedSuffix ) ;
785+ let attemptBody = passthroughBody ;
786+ if ( normalizedSuffix === 'chat/completions' && attemptBody && typeof attemptBody === 'object' && ! Array . isArray ( attemptBody ) ) {
787+ attemptBody = { ...attemptBody } ;
788+ if ( ! attemptBody . model && attemptEntry . model ) {
789+ attemptBody . model = attemptEntry . model ;
790+ }
791+ if ( attemptBody . temperature === undefined && attemptEntry . temperature !== null && attemptEntry . temperature !== undefined ) {
792+ attemptBody . temperature = attemptEntry . temperature ;
793+ }
794+ }
795+ attemptedProviders . push ( attemptEntry . name ) ;
796+ attemptedProviderNames . add ( attemptEntry . name ) ;
797+ const upstreamResult = await retryTransientRequest ( ( ) => proxyRequestJson ( upstreamUrl , {
798+ method,
799+ body : attemptBody ,
800+ headers : {
801+ ...( attemptAuthHeader ? { Authorization : attemptAuthHeader } : { } ) ,
802+ ...( attemptBody !== null ? { 'Content-Type' : 'application/json' } : { } )
803+ } ,
804+ maxBytes : maxUpstreamBytes ,
805+ httpAgent,
806+ httpsAgent
807+ } ) ) ;
808+ lastResult = upstreamResult ;
809+ lastEntry = attemptEntry ;
810+ if ( ! shouldFailover ( upstreamResult ) ) {
811+ recordSuccess ( attemptEntry . name ) ;
812+ res . writeHead ( upstreamResult . status , {
813+ 'Content-Type' : 'application/json; charset=utf-8' ,
814+ 'X-Codexmate-Upstream-Provider' : attemptEntry . name ,
815+ 'X-Codexmate-Attempted-Providers' : attemptedProviders . join ( ',' )
816+ } ) ;
817+ res . end ( upstreamResult . bodyText ) ;
818+ return ;
819+ }
820+ recordFailure ( attemptEntry . name ) ;
821+ }
822+
823+ if ( ! lastResult || ! lastResult . ok ) {
824+ res . writeHead ( 502 , {
825+ 'Content-Type' : 'application/json; charset=utf-8' ,
826+ 'X-Codexmate-Attempted-Providers' : attemptedProviders . join ( ',' )
827+ } ) ;
828+ res . end ( JSON . stringify ( { error : `Upstream request failed: ${ lastResult && lastResult . error ? lastResult . error : 'unknown error' } ` } ) ) ;
739829 return ;
740830 }
741- recordSuccess ( entry . name ) ;
742- res . writeHead ( upstreamResult . status , { 'Content-Type' : 'application/json; charset=utf-8' } ) ;
743- res . end ( upstreamResult . bodyText ) ;
831+ res . writeHead ( lastResult . status || 502 , {
832+ 'Content-Type' : 'application/json; charset=utf-8' ,
833+ 'X-Codexmate-Upstream-Provider' : lastEntry ? lastEntry . name : '' ,
834+ 'X-Codexmate-Attempted-Providers' : attemptedProviders . join ( ',' )
835+ } ) ;
836+ res . end ( lastResult . bodyText || JSON . stringify ( { error : 'Upstream error' } ) ) ;
744837 } catch ( e ) {
745838 res . writeHead ( 500 , { 'Content-Type' : 'application/json; charset=utf-8' } ) ;
746839 res . end ( JSON . stringify ( { error : e && e . message ? e . message : 'Internal Error' } ) ) ;
0 commit comments