2026-01-11 09:45:01 -05:00
import { BaseExecutor } from "./base.js" ;
2026-06-05 23:36:03 -04:00
import { PROVIDERS } from "../config/providers.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-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" ) {
if ( this . provider === "cerebras" || this . provider === "mistral" ) {
delete transformed . client _metadata ;
}
}
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 ) {
if ( this . provider ? . startsWith ? . ( "openai-compatible-" ) ) {
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || "https://api.openai.com/v1" ;
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-" ) ) {
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || "https://api.anthropic.com/v1" ;
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
}
buildHeaders ( credentials , stream = true ) {
const headers = { "Content-Type" : "application/json" , ... this . config . headers } ;
switch ( this . provider ) {
case "gemini" :
credentials . apiKey ? headers [ "x-goog-api-key" ] = credentials . apiKey : headers [ "Authorization" ] = ` Bearer ${ credentials . accessToken } ` ;
break ;
2026-03-25 05:57:26 -04:00
case "claude" : {
// Overlay live cached headers from real Claude Code client over static defaults.
// Static headers (Title-Case) remain as cold-start fallback.
const cached = getCachedClaudeHeaders ( ) ;
if ( cached ) {
// Remove Title-Case static keys that conflict with incoming lowercase cached keys
for ( const lcKey of Object . keys ( cached ) ) {
// Build the Title-Case equivalent: "anthropic-version" → "Anthropic-Version"
const titleKey = lcKey . replace ( /(^|-)([a-z])/g , ( _ , sep , c ) => sep + c . toUpperCase ( ) ) ;
// Special handling for Anthropic-Beta to preserve required flags like OAuth
if ( lcKey === "anthropic-beta" ) {
const staticBetaStr = headers [ titleKey ] || headers [ lcKey ] || "" ;
const staticFlags = new Set ( staticBetaStr . split ( "," ) . map ( f => f . trim ( ) ) . filter ( Boolean ) ) ;
const cachedFlags = new Set ( cached [ lcKey ] . split ( "," ) . map ( f => f . trim ( ) ) . filter ( Boolean ) ) ;
// Merge all static flags (which contain oauth, thinking, etc) into the cached ones
for ( const flag of staticFlags ) {
cachedFlags . add ( flag ) ;
}
cached [ lcKey ] = Array . from ( cachedFlags ) . join ( "," ) ;
}
if ( titleKey !== lcKey && headers [ titleKey ] !== undefined ) {
delete headers [ titleKey ] ;
}
}
Object . assign ( headers , cached ) ;
}
credentials . apiKey
? ( headers [ "x-api-key" ] = credentials . apiKey )
: ( headers [ "Authorization" ] = ` Bearer ${ credentials . accessToken } ` ) ;
2026-01-11 09:45:01 -05:00
break ;
2026-03-25 05:57:26 -04:00
}
2026-01-11 09:45:01 -05:00
case "glm" :
case "kimi" :
case "minimax" :
2026-02-06 03:10:57 -05:00
case "minimax-cn" :
2026-03-11 23:48:11 -04:00
case "kimi-coding" :
2026-05-11 22:19:50 -04:00
headers [ "x-api-key" ] = credentials . apiKey || credentials . accessToken ;
if ( this . provider === "kimi-coding" ) Object . assign ( headers , buildKimiHeaders ( ) ) ;
2026-03-11 23:48:11 -04:00
break ;
2026-01-11 09:45:01 -05:00
default :
2026-02-03 03:11:41 -05:00
if ( this . provider ? . startsWith ? . ( "anthropic-compatible-" ) ) {
if ( credentials . apiKey ) {
headers [ "x-api-key" ] = credentials . apiKey ;
} else if ( credentials . accessToken ) {
headers [ "Authorization" ] = ` Bearer ${ credentials . accessToken } ` ;
}
if ( ! headers [ "anthropic-version" ] ) {
headers [ "anthropic-version" ] = "2023-06-01" ;
}
2026-03-30 00:28:07 -04:00
} else if ( this . provider === "gitlab" ) {
// GitLab Duo uses Bearer token (PAT with ai_features scope, or OAuth access token)
headers [ "Authorization" ] = ` Bearer ${ credentials . apiKey || credentials . accessToken } ` ;
} else if ( this . provider === "codebuddy" ) {
headers [ "Authorization" ] = ` Bearer ${ credentials . apiKey || credentials . accessToken } ` ;
2026-02-27 23:30:21 -05:00
} else if ( this . provider === "kilocode" ) {
headers [ "Authorization" ] = ` Bearer ${ credentials . apiKey || credentials . accessToken } ` ;
if ( credentials . providerSpecificData ? . orgId ) {
headers [ "X-Kilocode-OrganizationID" ] = credentials . providerSpecificData . orgId ;
}
2026-03-09 05:21:29 -04:00
} else if ( this . provider === "cline" ) {
Object . assign ( headers , buildClineHeaders ( credentials . apiKey || credentials . accessToken ) ) ;
2026-05-11 22:19:50 -04:00
} else if ( this . config ? . format === "claude" ) {
// Generic claude-format provider (e.g. agentrouter): x-api-key + anthropic-version
headers [ "x-api-key" ] = credentials . apiKey || credentials . accessToken ;
if ( ! headers [ "anthropic-version" ] ) headers [ "anthropic-version" ] = "2023-06-01" ;
2026-02-03 03:11:41 -05:00
} else {
headers [ "Authorization" ] = ` Bearer ${ credentials . apiKey || credentials . accessToken } ` ;
}
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-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-04-28 06:28:57 -04:00
claude : ( ) => this . refreshWithJSON ( OAUTH _ENDPOINTS . anthropic . token , { grant _type : "refresh_token" , refresh _token : credentials . refreshToken , client _id : PROVIDERS . claude . clientId } , proxyOptions ) ,
codex : ( ) => this . refreshWithForm ( OAUTH _ENDPOINTS . openai . token , { grant _type : "refresh_token" , refresh _token : credentials . refreshToken , client _id : PROVIDERS . codex . clientId , scope : "openid profile email offline_access" } , proxyOptions ) ,
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 ) ,
gemini : ( ) => this . refreshGoogle ( credentials . refreshToken , proxyOptions ) ,
kiro : ( ) => this . refreshKiro ( credentials . refreshToken , proxyOptions ) ,
cline : ( ) => this . refreshCline ( credentials . refreshToken , proxyOptions ) ,
"kimi-coding" : ( ) => this . refreshKimiCoding ( credentials . refreshToken , proxyOptions ) ,
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 refreshGoogle ( refreshToken , proxyOptions = null ) {
const response = await proxyAwareFetch ( OAUTH _ENDPOINTS . google . token , {
2026-01-11 09:45:01 -05:00
method : "POST" ,
headers : { "Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "application/json" } ,
body : new URLSearchParams ( { grant _type : "refresh_token" , refresh _token : refreshToken , client _id : this . config . clientId , client _secret : this . config . 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-01-15 06:29:47 -05:00
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 ;
return { accessToken : data ? . accessToken , refreshToken : data ? . refreshToken || refreshToken , expiresIn } ;
}
2026-04-28 06:28:57 -04:00
async refreshKimiCoding ( refreshToken , proxyOptions = null ) {
2026-03-11 23:48:11 -04:00
const kimiHeaders = buildKimiHeaders ( ) ;
2026-06-13 09:33:38 -04:00
const response = await proxyAwareFetch ( PROVIDERS [ "kimi-coding" ] . 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-06-13 09:33:38 -04:00
body : new URLSearchParams ( { grant _type : "refresh_token" , refresh _token : refreshToken , client _id : PROVIDERS [ "kimi-coding" ] . 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 ;