2026-01-11 09:45:01 -05:00
import { BaseExecutor } from "./base.js" ;
2026-06-14 02:15:48 -04:00
import { PROVIDERS , PROVIDER _OAUTH } from "../config/providers.js" ;
import { ANTHROPIC _API _VERSION , OPENAI _COMPAT _BASE , ANTHROPIC _COMPAT _BASE } from "../providers/shared.js" ;
2026-03-12 05:20:46 -04:00
import { OAUTH _ENDPOINTS , buildKimiHeaders } from "../config/appConstants.js" ;
2026-06-13 05:35:07 -04:00
import { buildClineHeaders } from "../shared/clineAuth.js" ;
2026-03-25 05:57:26 -04:00
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js" ;
2026-04-28 06:28:57 -04:00
import { proxyAwareFetch } from "../utils/proxyFetch.js" ;
2026-04-28 22:34:24 -04:00
import { injectReasoningContent } from "../utils/reasoningContentInjector.js" ;
2026-06-16 23:34:53 -04:00
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js" ;
2026-01-11 09:45:01 -05:00
2026-06-14 02:15:48 -04:00
// Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults.
const BEARER = { combined : true , header : "Authorization" , scheme : "bearer" } ;
const XAPIKEY = { combined : true , header : "x-api-key" , scheme : "raw" } ;
const AUTH _DESCRIPTORS = Object . fromEntries (
Object . entries ( PROVIDERS )
. filter ( ( [ , t ] ) => t . auth )
. map ( ( [ id , t ] ) => [ id , t . auth ] )
) ;
// Apply a token to a header per scheme (matches legacy: combined always sets, even when undefined).
function setAuth ( headers , spec , token ) {
headers [ spec . header ] = spec . scheme === "bearer" ? ` Bearer ${ token } ` : token ;
}
// Resolve auth onto headers from a descriptor.
function applyAuth ( headers , desc , credentials ) {
if ( desc . combined ) {
// combined providers always set the header (legacy behavior, incl. noAuth → "Bearer undefined")
setAuth ( headers , desc , credentials . apiKey || credentials . accessToken ) ;
if ( desc . anthropicVersion && ! headers [ "anthropic-version" ] ) headers [ "anthropic-version" ] = ANTHROPIC _API _VERSION ;
return ;
}
// split apiKey/oauth: set only the matching branch (legacy: anthropic-compatible skips when both absent)
if ( credentials . apiKey ) setAuth ( headers , desc . apiKey , credentials . apiKey ) ;
else if ( credentials . accessToken ) setAuth ( headers , desc . oauth , credentials . accessToken ) ;
if ( desc . anthropicVersion && ! headers [ "anthropic-version" ] ) headers [ "anthropic-version" ] = ANTHROPIC _API _VERSION ;
}
// Provider-specific header quirks kept as small hooks (not pure auth).
const HEADER _HOOKS = {
2026-07-17 01:09:14 -04:00
// Stable device_id from OAuth connection (CLIProxyAPI KimiTokenStorage.DeviceID)
kimiHeaders : ( h , c ) => Object . assign ( h , buildKimiHeaders ( c ? . providerSpecificData ? . deviceId ) ) ,
2026-06-14 02:15:48 -04:00
clineHeaders : ( h , c ) => Object . assign ( h , buildClineHeaders ( c . apiKey || c . accessToken ) ) ,
kilocodeOrg : ( h , c ) => { if ( c . providerSpecificData ? . orgId ) h [ "X-Kilocode-OrganizationID" ] = c . providerSpecificData . orgId ; } ,
claudeOverlay : ( h ) => {
const cached = getCachedClaudeHeaders ( ) ;
if ( ! cached ) return ;
for ( const lcKey of Object . keys ( cached ) ) {
const titleKey = lcKey . replace ( /(^|-)([a-z])/g , ( _ , sep , ch ) => sep + ch . toUpperCase ( ) ) ;
if ( lcKey === "anthropic-beta" ) {
const staticBetaStr = h [ titleKey ] || h [ lcKey ] || "" ;
const flags = new Set ( staticBetaStr . split ( "," ) . map ( f => f . trim ( ) ) . filter ( Boolean ) ) ;
for ( const f of cached [ lcKey ] . split ( "," ) . map ( f => f . trim ( ) ) . filter ( Boolean ) ) flags . add ( f ) ;
cached [ lcKey ] = Array . from ( flags ) . join ( "," ) ;
}
if ( titleKey !== lcKey && h [ titleKey ] !== undefined ) delete h [ titleKey ] ;
}
Object . assign ( h , cached ) ;
} ,
} ;
// Config-driven OAuth refresh grants — derived from registry oauth.refresh.
const REFRESH _GRANTS = Object . fromEntries (
Object . entries ( PROVIDER _OAUTH )
. filter ( ( [ , o ] ) => o . refresh )
. map ( ( [ id , o ] ) => {
const tokenUrl = o . tokenUrl ;
const encoding = o . refresh . encoding ;
const extraParams = o . refresh . scope ? { scope : o . refresh . scope } : { } ;
return [ id , {
encoding ,
url : ( ) => tokenUrl ,
params : ( ex ) => id === "gemini"
? { client _id : ex . config . clientId , client _secret : ex . config . clientSecret , ... extraParams }
: { client _id : o . clientId , ... extraParams } ,
} ] ;
} )
) ;
2026-01-11 09:45:01 -05:00
export class DefaultExecutor extends BaseExecutor {
constructor ( provider ) {
super ( provider , PROVIDERS [ provider ] || PROVIDERS . openai ) ;
}
2026-04-28 22:34:24 -04:00
transformRequest ( model , body ) {
2026-05-26 00:22:31 -04:00
const transformed = this . applyJsonSchemaFallback ( body ) ;
2026-06-13 00:43:12 -04:00
if ( transformed && typeof transformed === "object" ) {
2026-06-14 02:15:48 -04:00
// quirk: some openai-compatible providers reject Anthropic's client_metadata field
if ( this . config . quirks ? . dropClientMetadata ) {
2026-06-13 00:43:12 -04:00
delete transformed . client _metadata ;
}
2026-06-16 23:34:53 -04:00
stripUnsupportedParams ( this . provider , model , transformed ) ;
2026-06-13 00:43:12 -04:00
}
2026-05-26 00:22:31 -04:00
return injectReasoningContent ( { provider : this . provider , model , body : transformed } ) ;
}
// Fallback json_schema → json_object for openai-compatible providers without native Structured Output.
applyJsonSchemaFallback ( body ) {
if ( ! this . provider ? . startsWith ? . ( "openai-compatible-" ) ) return body ;
const rf = body ? . response _format ;
if ( rf ? . type !== "json_schema" || ! rf . json _schema ? . schema ) return body ;
const schemaJson = JSON . stringify ( rf . json _schema . schema , null , 2 ) ;
const prompt = ` You must respond with valid JSON that strictly follows this JSON schema: \n \` \` \` json \n ${ schemaJson } \n \` \` \` \n Respond ONLY with the JSON object, no other text. ` ;
const messages = Array . isArray ( body . messages ) ? body . messages . map ( m => ( { ... m } ) ) : [ ] ;
const sys = messages . find ( m => m . role === "system" ) ;
if ( sys ) {
if ( typeof sys . content === "string" ) sys . content = ` ${ sys . content } \n \n ${ prompt } ` ;
else if ( Array . isArray ( sys . content ) ) sys . content . push ( { type : "text" , text : ` \n \n ${ prompt } ` } ) ;
} else {
messages . unshift ( { role : "system" , content : prompt } ) ;
}
return { ... body , messages , response _format : { type : "json_object" } } ;
2026-04-28 22:34:24 -04:00
}
2026-02-02 07:45:12 -05:00
buildUrl ( model , stream , urlIndex = 0 , credentials = null ) {
2026-06-19 23:09:50 -04:00
// Runtime transport (multi-endpoint providers): use the sourceFormat-matched endpoint
const rt = credentials ? . runtimeTransport ;
if ( rt ? . baseUrl ) {
return rt . urlSuffix ? ` ${ rt . baseUrl } ${ rt . urlSuffix } ` : rt . baseUrl ;
}
2026-02-02 07:45:12 -05:00
if ( this . provider ? . startsWith ? . ( "openai-compatible-" ) ) {
2026-06-14 02:15:48 -04:00
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || OPENAI _COMPAT _BASE ;
2026-02-02 07:45:12 -05:00
const normalized = baseUrl . replace ( /\/$/ , "" ) ;
const path = this . provider . includes ( "responses" ) ? "/responses" : "/chat/completions" ;
return ` ${ normalized } ${ path } ` ;
}
2026-02-03 03:11:41 -05:00
if ( this . provider ? . startsWith ? . ( "anthropic-compatible-" ) ) {
2026-06-14 02:15:48 -04:00
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || ANTHROPIC _COMPAT _BASE ;
2026-02-03 03:11:41 -05:00
const normalized = baseUrl . replace ( /\/$/ , "" ) ;
return ` ${ normalized } /messages ` ;
}
2026-06-13 11:00:57 -04:00
// gemini-format: build :streamGenerateContent / :generateContent path
if ( this . config . format === "gemini" ) {
return ` ${ this . config . baseUrl } / ${ model } : ${ stream ? "streamGenerateContent?alt=sse" : "generateContent" } ` ;
}
// urlSuffix (e.g. ?beta=true) declared per-provider in registry
if ( this . config . urlSuffix ) {
return ` ${ this . config . baseUrl } ${ this . config . urlSuffix } ` ;
}
const url = this . config . baseUrl ;
if ( url ? . includes ( "{accountId}" ) ) {
const accountId = credentials ? . providerSpecificData ? . accountId ;
if ( ! accountId ) throw new Error ( ` ${ this . provider } requires accountId in providerSpecificData ` ) ;
return url . replace ( "{accountId}" , accountId ) ;
2026-01-11 09:45:01 -05:00
}
2026-06-13 11:00:57 -04:00
return url ;
2026-01-11 09:45:01 -05:00
}
2026-06-14 02:15:48 -04:00
// Fallback descriptor for providers without an explicit entry in AUTH_DESCRIPTORS.
resolveAuthDescriptor ( ) {
if ( this . provider ? . startsWith ? . ( "anthropic-compatible-" ) ) {
return { apiKey : { header : "x-api-key" , scheme : "raw" } , oauth : { header : "Authorization" , scheme : "bearer" } , anthropicVersion : true } ;
}
if ( this . config ? . format === "claude" ) {
return { ... XAPIKEY , anthropicVersion : true } ;
}
return BEARER ;
}
2026-01-11 09:45:01 -05:00
buildHeaders ( credentials , stream = true ) {
2026-06-19 23:09:50 -04:00
const rt = credentials ? . runtimeTransport ;
const headers = { "Content-Type" : "application/json" , ... ( rt ? rt . headers : this . config . headers ) } ;
const desc = rt ? . auth || AUTH _DESCRIPTORS [ this . provider ] || this . resolveAuthDescriptor ( ) ;
2026-06-14 02:15:48 -04:00
// Hooks run BEFORE auth so dynamic overlays (claude cached headers) can't clobber the token.
for ( const hook of desc . hooks || [ ] ) HEADER _HOOKS [ hook ] ? . ( headers , credentials ) ;
applyAuth ( headers , desc , credentials ) ;
2026-01-11 09:45:01 -05:00
2026-03-25 05:57:26 -04:00
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
if ( this . provider ? . startsWith ? . ( "anthropic-compatible-" ) ) {
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || "" ;
const isOfficialAnthropic = baseUrl === "" || baseUrl . includes ( "api.anthropic.com" ) ;
if ( ! isOfficialAnthropic ) {
2026-06-12 22:45:59 -04:00
// Some third-party Anthropic-compatible gateways require Bearer auth in
// addition to x-api-key. Send both (x-api-key already set above) so
// gateways that read either header succeed.
if ( credentials . apiKey && ! headers [ "Authorization" ] ) {
headers [ "Authorization" ] = ` Bearer ${ credentials . apiKey } ` ;
}
2026-03-25 05:57:26 -04:00
delete headers [ "anthropic-dangerous-direct-browser-access" ] ;
delete headers [ "Anthropic-Dangerous-Direct-Browser-Access" ] ;
delete headers [ "x-app" ] ;
delete headers [ "X-App" ] ;
// Strip claude-code-20250219 from Anthropic-Beta / anthropic-beta
for ( const betaKey of [ "anthropic-beta" , "Anthropic-Beta" ] ) {
if ( headers [ betaKey ] ) {
const filtered = headers [ betaKey ]
. split ( "," )
. map ( s => s . trim ( ) )
. filter ( f => f && f !== "claude-code-20250219" )
. join ( "," ) ;
if ( filtered ) {
headers [ betaKey ] = filtered ;
} else {
delete headers [ betaKey ] ;
}
}
}
}
}
2026-01-11 09:45:01 -05:00
if ( stream ) headers [ "Accept" ] = "text/event-stream" ;
return headers ;
}
2026-06-14 02:15:48 -04:00
// Generic OAuth refresh for the common {grant_type, refresh_token, client_id[, ...]} shape.
// grant = REFRESH_GRANTS[provider]; client creds resolved from PROVIDERS or this.config.
refreshFromGrant ( credentials , proxyOptions ) {
const grant = REFRESH _GRANTS [ this . provider ] ;
const params = { grant _type : "refresh_token" , refresh _token : credentials . refreshToken , ... grant . params ( this ) } ;
return grant . encoding === "json"
? this . refreshWithJSON ( grant . url ( ) , params , proxyOptions )
: this . refreshWithForm ( grant . url ( ) , params , proxyOptions ) ;
}
2026-04-28 06:28:57 -04:00
async refreshCredentials ( credentials , log , proxyOptions = null ) {
2026-01-11 09:45:01 -05:00
if ( ! credentials . refreshToken ) return null ;
const refreshers = {
2026-06-14 02:15:48 -04:00
claude : ( ) => this . refreshFromGrant ( credentials , proxyOptions ) ,
codex : ( ) => this . refreshFromGrant ( credentials , proxyOptions ) ,
2026-04-28 06:28:57 -04:00
qwen : ( ) => this . refreshWithForm ( OAUTH _ENDPOINTS . qwen . token , { grant _type : "refresh_token" , refresh _token : credentials . refreshToken , client _id : PROVIDERS . qwen . clientId } , proxyOptions ) ,
iflow : ( ) => this . refreshIflow ( credentials . refreshToken , proxyOptions ) ,
2026-06-14 02:15:48 -04:00
gemini : ( ) => this . refreshFromGrant ( credentials , proxyOptions ) ,
2026-04-28 06:28:57 -04:00
kiro : ( ) => this . refreshKiro ( credentials . refreshToken , proxyOptions ) ,
cline : ( ) => this . refreshCline ( credentials . refreshToken , proxyOptions ) ,
2026-07-02 23:50:32 -04:00
clinepass : ( ) => this . refreshCline ( credentials . refreshToken , proxyOptions ) ,
2026-07-17 01:09:14 -04:00
kimi : ( ) => this . refreshKimi ( credentials , proxyOptions ) ,
"kimi-coding" : ( ) => this . refreshKimi ( credentials , proxyOptions ) ,
2026-04-28 06:28:57 -04:00
kilocode : ( ) => this . refreshKilocode ( credentials . refreshToken , proxyOptions )
2026-01-11 09:45:01 -05:00
} ;
const refresher = refreshers [ this . provider ] ;
if ( ! refresher ) return null ;
try {
const result = await refresher ( ) ;
if ( result ) log ? . info ? . ( "TOKEN" , ` ${ this . provider } refreshed ` ) ;
return result ;
} catch ( error ) {
log ? . error ? . ( "TOKEN" , ` ${ this . provider } refresh error: ${ error . message } ` ) ;
return null ;
}
}
2026-04-28 06:28:57 -04:00
async refreshWithJSON ( url , body , proxyOptions = null ) {
const response = await proxyAwareFetch ( url , {
2026-01-11 09:45:01 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/json" , "Accept" : "application/json" } ,
body : JSON . stringify ( body )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-01-11 09:45:01 -05:00
if ( ! response . ok ) return null ;
const tokens = await response . json ( ) ;
return { accessToken : tokens . access _token , refreshToken : tokens . refresh _token || body . refresh _token , expiresIn : tokens . expires _in } ;
}
2026-04-28 06:28:57 -04:00
async refreshWithForm ( url , params , proxyOptions = null ) {
const response = await proxyAwareFetch ( url , {
2026-01-11 09:45:01 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "application/json" } ,
body : new URLSearchParams ( params )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-01-11 09:45:01 -05:00
if ( ! response . ok ) return null ;
const tokens = await response . json ( ) ;
return { accessToken : tokens . access _token , refreshToken : tokens . refresh _token || params . refresh _token , expiresIn : tokens . expires _in } ;
}
2026-04-28 06:28:57 -04:00
async refreshIflow ( refreshToken , proxyOptions = null ) {
2026-01-11 09:45:01 -05:00
const basicAuth = btoa ( ` ${ PROVIDERS . iflow . clientId } : ${ PROVIDERS . iflow . clientSecret } ` ) ;
2026-04-28 06:28:57 -04:00
const response = await proxyAwareFetch ( OAUTH _ENDPOINTS . iflow . token , {
2026-01-11 09:45:01 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "application/json" , "Authorization" : ` Basic ${ basicAuth } ` } ,
body : new URLSearchParams ( { grant _type : "refresh_token" , refresh _token : refreshToken , client _id : PROVIDERS . iflow . clientId , client _secret : PROVIDERS . iflow . clientSecret } )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-01-11 09:45:01 -05:00
if ( ! response . ok ) return null ;
const tokens = await response . json ( ) ;
return { accessToken : tokens . access _token , refreshToken : tokens . refresh _token || refreshToken , expiresIn : tokens . expires _in } ;
}
2026-04-28 06:28:57 -04:00
async refreshKiro ( refreshToken , proxyOptions = null ) {
const response = await proxyAwareFetch ( PROVIDERS . kiro . tokenUrl , {
2026-01-15 06:29:47 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/json" , "Accept" : "application/json" , "User-Agent" : "kiro-cli/1.0.0" } ,
body : JSON . stringify ( { refreshToken } )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-01-15 06:29:47 -05:00
if ( ! response . ok ) return null ;
const tokens = await response . json ( ) ;
return { accessToken : tokens . accessToken , refreshToken : tokens . refreshToken || refreshToken , expiresIn : tokens . expiresIn } ;
}
2026-02-20 05:05:46 -05:00
2026-04-28 06:28:57 -04:00
async refreshCline ( refreshToken , proxyOptions = null ) {
2026-06-13 09:33:38 -04:00
const response = await proxyAwareFetch ( PROVIDERS . cline . refreshUrl , {
2026-02-20 05:05:46 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/json" , "Accept" : "application/json" } ,
body : JSON . stringify ( { refreshToken , grantType : "refresh_token" , clientType : "extension" } )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-06-13 11:00:57 -04:00
if ( ! response . ok ) return null ;
2026-02-20 05:05:46 -05:00
const payload = await response . json ( ) ;
const data = payload ? . data || payload ;
const expiresAtIso = data ? . expiresAt ;
const expiresIn = expiresAtIso ? Math . max ( 1 , Math . floor ( ( new Date ( expiresAtIso ) . getTime ( ) - Date . now ( ) ) / 1000 ) ) : undefined ;
2026-07-02 23:50:32 -04:00
let accessToken = data ? . accessToken ;
if ( accessToken && ! accessToken . startsWith ( "workos:" ) ) {
accessToken = ` workos: ${ accessToken } ` ;
}
return { accessToken , refreshToken : data ? . refreshToken || refreshToken , expiresIn } ;
2026-02-20 05:05:46 -05:00
}
2026-07-17 01:09:14 -04:00
// CLIProxyAPI DeviceFlowClient.RefreshToken — form body + X-Msh-* headers + stable device_id
async refreshKimi ( credentials , proxyOptions = null ) {
const refreshToken = credentials . refreshToken ;
const cfg = PROVIDERS . kimi || PROVIDERS [ "kimi-coding" ] ;
if ( ! cfg ? . refreshUrl || ! cfg ? . clientId ) return null ;
const kimiHeaders = buildKimiHeaders ( credentials ? . providerSpecificData ? . deviceId ) ;
const response = await proxyAwareFetch ( cfg . refreshUrl , {
2026-02-20 05:05:46 -05:00
method : "POST" ,
2026-03-25 05:57:26 -04:00
headers : {
"Content-Type" : "application/x-www-form-urlencoded" ,
2026-03-11 23:48:11 -04:00
"Accept" : "application/json" ,
... kimiHeaders
} ,
2026-07-17 01:09:14 -04:00
body : new URLSearchParams ( { grant _type : "refresh_token" , refresh _token : refreshToken , client _id : cfg . clientId } )
2026-04-28 06:28:57 -04:00
} , proxyOptions ) ;
2026-02-20 05:05:46 -05:00
if ( ! response . ok ) return null ;
const tokens = await response . json ( ) ;
return { accessToken : tokens . access _token , refreshToken : tokens . refresh _token || refreshToken , expiresIn : tokens . expires _in } ;
}
2026-04-28 06:28:57 -04:00
async refreshKilocode ( refreshToken , proxyOptions = null ) {
2026-02-20 05:05:46 -05:00
// Kilocode uses device code flow, no refresh token support
return null ;
}
2026-01-11 09:45:01 -05:00
}
export default DefaultExecutor ;