2026-01-11 21:45:01 +07:00
import { BaseExecutor } from "./base.js" ;
2026-06-06 10:36:03 +07:00
import { PROVIDERS } from "../config/providers.js" ;
2026-03-12 16:20:46 +07:00
import { OAUTH_ENDPOINTS , buildKimiHeaders } from "../config/appConstants.js" ;
2026-03-09 16:21:29 +07:00
import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js" ;
2026-03-25 16:57:26 +07:00
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js" ;
2026-04-28 17:28:57 +07:00
import { proxyAwareFetch } from "../utils/proxyFetch.js" ;
2026-04-29 09:34:24 +07:00
import { injectReasoningContent } from "../utils/reasoningContentInjector.js" ;
2026-01-11 21:45:01 +07:00
export class DefaultExecutor extends BaseExecutor {
constructor ( provider ) {
super ( provider , PROVIDERS [ provider ] || PROVIDERS . openai );
}
2026-04-29 09:34:24 +07:00
transformRequest ( model , body ) {
2026-05-26 11:22:31 +07:00
const transformed = this . applyJsonSchemaFallback ( body );
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\`\`\`\nRespond 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-29 09:34:24 +07:00
}
2026-02-02 19:45:12 +07: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 15:11:41 +07: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-01-11 21:45:01 +07:00
switch ( this . provider ) {
case "claude" :
case "glm" :
case "kimi" :
case "minimax" :
2026-02-06 15:10:57 +07:00
case "minimax-cn" :
2026-01-11 21:45:01 +07:00
return ` ${ this . config . baseUrl } ?beta=true` ;
2026-03-12 10:48:11 +07:00
case "kimi-coding" :
return ` ${ this . config . baseUrl } ?beta=true` ;
2026-01-11 21:45:01 +07:00
case "gemini" :
return ` ${ this . config . baseUrl } / ${ model } : ${ stream ? "streamGenerateContent?alt=sse" : "generateContent" } ` ;
2026-04-28 11:07:39 +07:00
default : {
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 );
}
return url ;
}
2026-01-11 21:45:01 +07: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 16:57:26 +07: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 21:45:01 +07:00
break ;
2026-03-25 16:57:26 +07:00
}
2026-01-11 21:45:01 +07:00
case "glm" :
case "kimi" :
case "minimax" :
2026-02-06 15:10:57 +07:00
case "minimax-cn" :
2026-03-12 10:48:11 +07:00
case "kimi-coding" :
2026-05-12 09:19:50 +07:00
headers [ "x-api-key" ] = credentials . apiKey || credentials . accessToken ;
if ( this . provider === "kimi-coding" ) Object . assign ( headers , buildKimiHeaders ());
2026-03-12 10:48:11 +07:00
break ;
2026-01-11 21:45:01 +07:00
default :
2026-02-03 15:11:41 +07: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 11:28:07 +07: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-28 11:30:21 +07: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 16:21:29 +07:00
} else if ( this . provider === "cline" ) {
Object . assign ( headers , buildClineHeaders ( credentials . apiKey || credentials . accessToken ));
2026-05-12 09:19:50 +07: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 15:11:41 +07:00
} else {
headers [ "Authorization" ] = `Bearer ${ credentials . apiKey || credentials . accessToken } ` ;
}
2026-01-11 21:45:01 +07:00
}
2026-03-25 16:57:26 +07: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 ) {
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 21:45:01 +07:00
if ( stream ) headers [ "Accept" ] = "text/event-stream" ;
return headers ;
}
2026-04-28 17:28:57 +07:00
async refreshCredentials ( credentials , log , proxyOptions = null ) {
2026-01-11 21:45:01 +07:00
if ( ! credentials . refreshToken ) return null ;
const refreshers = {
2026-04-28 17:28:57 +07: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 21:45:01 +07: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 17:28:57 +07:00
async refreshWithJSON ( url , body , proxyOptions = null ) {
const response = await proxyAwareFetch ( url , {
2026-01-11 21:45:01 +07:00
method : "POST" ,
headers : { "Content-Type" : "application/json" , "Accept" : "application/json" },
body : JSON . stringify ( body )
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-11 21:45:01 +07: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 17:28:57 +07:00
async refreshWithForm ( url , params , proxyOptions = null ) {
const response = await proxyAwareFetch ( url , {
2026-01-11 21:45:01 +07:00
method : "POST" ,
headers : { "Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "application/json" },
body : new URLSearchParams ( params )
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-11 21:45:01 +07: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 17:28:57 +07:00
async refreshIflow ( refreshToken , proxyOptions = null ) {
2026-01-11 21:45:01 +07:00
const basicAuth = btoa ( ` ${ PROVIDERS . iflow . clientId } : ${ PROVIDERS . iflow . clientSecret } ` );
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( OAUTH_ENDPOINTS . iflow . token , {
2026-01-11 21:45:01 +07: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 17:28:57 +07:00
}, proxyOptions );
2026-01-11 21:45:01 +07: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 17:28:57 +07:00
async refreshGoogle ( refreshToken , proxyOptions = null ) {
const response = await proxyAwareFetch ( OAUTH_ENDPOINTS . google . token , {
2026-01-11 21:45:01 +07: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 17:28:57 +07:00
}, proxyOptions );
2026-01-11 21:45:01 +07: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 18:29:47 +07:00
2026-04-28 17:28:57 +07:00
async refreshKiro ( refreshToken , proxyOptions = null ) {
const response = await proxyAwareFetch ( PROVIDERS . kiro . tokenUrl , {
2026-01-15 18:29:47 +07: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 17:28:57 +07:00
}, proxyOptions );
2026-01-15 18:29:47 +07: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 17:05:46 +07:00
2026-04-28 17:28:57 +07:00
async refreshCline ( refreshToken , proxyOptions = null ) {
2026-02-20 17:05:46 +07:00
console . log ( '[DEBUG] Refreshing Cline token, refreshToken length:' , refreshToken ? . length );
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( "https://api.cline.bot/api/v1/auth/refresh" , {
2026-02-20 17:05:46 +07:00
method : "POST" ,
headers : { "Content-Type" : "application/json" , "Accept" : "application/json" },
body : JSON . stringify ({ refreshToken , grantType : "refresh_token" , clientType : "extension" })
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-02-20 17:05:46 +07:00
console . log ( '[DEBUG] Cline refresh response status:' , response . status );
if ( ! response . ok ) {
const errorText = await response . text ();
console . log ( '[DEBUG] Cline refresh error:' , errorText );
return null ;
}
const payload = await response . json ();
console . log ( '[DEBUG] Cline refresh payload:' , JSON . stringify ( payload ). substring ( 0 , 200 ));
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 ;
console . log ( '[DEBUG] Cline refresh success, expiresIn:' , expiresIn );
return { accessToken : data ? . accessToken , refreshToken : data ? . refreshToken || refreshToken , expiresIn };
}
2026-04-28 17:28:57 +07:00
async refreshKimiCoding ( refreshToken , proxyOptions = null ) {
2026-03-12 10:48:11 +07:00
const kimiHeaders = buildKimiHeaders ();
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( "https://auth.kimi.com/api/oauth/token" , {
2026-02-20 17:05:46 +07:00
method : "POST" ,
2026-03-25 16:57:26 +07:00
headers : {
"Content-Type" : "application/x-www-form-urlencoded" ,
2026-03-12 10:48:11 +07:00
"Accept" : "application/json" ,
... kimiHeaders
},
2026-02-20 17:05:46 +07:00
body : new URLSearchParams ({ grant_type : "refresh_token" , refresh_token : refreshToken , client_id : "17e5f671-d194-4dfb-9706-5516cb48c098" })
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-02-20 17:05:46 +07: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 17:28:57 +07:00
async refreshKilocode ( refreshToken , proxyOptions = null ) {
2026-02-20 17:05:46 +07:00
// Kilocode uses device code flow, no refresh token support
return null ;
}
2026-01-11 21:45:01 +07:00
}
export default DefaultExecutor ;