2026-01-11 21:45:01 +07:00
import { BaseExecutor } from "./base.js" ;
2026-06-14 13:15:48 +07:00
import { PROVIDERS , PROVIDER_OAUTH } from "../config/providers.js" ;
2026-08-05 11:32:14 +07:00
import { ANTHROPIC_API_VERSION , OPENAI_COMPAT_BASE , ANTHROPIC_COMPAT_BASE , selectAnthropicBeta } from "../providers/shared.js" ;
2026-08-05 13:23:03 +07:00
import { resolveOpenAICompatibleApiType } from "../services/provider.js" ;
2026-03-12 16:20:46 +07:00
import { OAUTH_ENDPOINTS , buildKimiHeaders } from "../config/appConstants.js" ;
2026-06-13 16:35:07 +07:00
import { buildClineHeaders } from "../shared/clineAuth.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-06-17 10:34:53 +07:00
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js" ;
2026-01-11 21:45:01 +07:00
2026-06-14 13:15:48 +07: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 12:09:14 +07:00
// Stable device_id from OAuth connection (CLIProxyAPI KimiTokenStorage.DeviceID)
kimiHeaders : ( h , c ) => Object . assign ( h , buildKimiHeaders ( c ? . providerSpecificData ? . deviceId )),
2026-06-14 13:15:48 +07: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 ; },
};
// 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 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 );
2026-06-13 11:43:12 +07:00
if ( transformed && typeof transformed === "object" ) {
2026-06-14 13:15:48 +07:00
// quirk: some openai-compatible providers reject Anthropic's client_metadata field
if ( this . config . quirks ? . dropClientMetadata ) {
2026-06-13 11:43:12 +07:00
delete transformed . client_metadata ;
}
2026-06-17 10:34:53 +07:00
stripUnsupportedParams ( this . provider , model , transformed );
2026-06-13 11:43:12 +07:00
}
2026-05-26 11:22:31 +07: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\`\`\`\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 ) {
2026-06-20 10:09:50 +07: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 19:45:12 +07:00
if ( this . provider ? . startsWith ? .( "openai-compatible-" )) {
2026-06-14 13:15:48 +07:00
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || OPENAI_COMPAT_BASE ;
2026-02-02 19:45:12 +07:00
const normalized = baseUrl . replace ( /\/$/ , "" );
2026-08-05 13:23:03 +07:00
const path = resolveOpenAICompatibleApiType ( this . provider , credentials ) === "responses" ? "/responses" : "/chat/completions" ;
2026-02-02 19:45:12 +07:00
return ` ${ normalized }${ path } ` ;
}
2026-02-03 15:11:41 +07:00
if ( this . provider ? . startsWith ? .( "anthropic-compatible-" )) {
2026-06-14 13:15:48 +07:00
const baseUrl = credentials ? . providerSpecificData ? . baseUrl || ANTHROPIC_COMPAT_BASE ;
2026-02-03 15:11:41 +07:00
const normalized = baseUrl . replace ( /\/$/ , "" );
return ` ${ normalized } /messages` ;
}
2026-06-13 22:00:57 +07:00
// gemini-format: build :streamGenerateContent / :generateContent path
if ( this . config . format === "gemini" ) {
return ` ${ this . config . baseUrl } / ${ model } : ${ stream ? "streamGenerateContent?alt=sse" : "generateContent" } ` ;
2026-01-11 21:45:01 +07:00
}
2026-06-13 22:00:57 +07:00
// 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 );
}
return url ;
2026-01-11 21:45:01 +07:00
}
2026-06-14 13:15:48 +07: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-09-17 18:11:20 +07:00
buildHeaders ( credentials , stream = true , url , model , body = null ) {
2026-06-20 10:09:50 +07: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-08-05 11:32:14 +07:00
// Hooks run BEFORE auth so dynamic overlays can't clobber the token.
2026-06-14 13:15:48 +07:00
for ( const hook of desc . hooks || []) HEADER_HOOKS [ hook ] ? .( headers , credentials );
applyAuth ( headers , desc , credentials );
2026-01-11 21:45:01 +07:00
2026-09-05 21:25:28 +07:00
// anthropic-compatible-* nodes serving a real Claude model sit in front of
// Anthropic itself (a rotating multi-account proxy, a corporate gateway),
// so the request needs the same beta flags the `claude` provider sends:
// without `context-management-2025-06-27` upstream rejects the
// `context_management` block Claude Code puts in every request with
// "context_management: Extra inputs are not permitted" (HTTP 400), and the
// combo silently falls through to the next model. The model id gates this:
// a node fronting Kimi or GLM answers on its own ids and never matches, so
// gateways that would choke on unknown beta flags are left untouched.
const isClaudeModel = typeof model === "string" && /^claude-/ . test ( model );
if ( model && ( this . provider === "claude"
|| ( this . provider ? . startsWith ? .( "anthropic-compatible-" ) && isClaudeModel ))) {
2026-09-17 18:11:20 +07:00
headers [ "Anthropic-Beta" ] = selectAnthropicBeta ( model , body );
2026-08-05 11:32:14 +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 ) {
2026-06-13 09:45:59 +07: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 16:57:26 +07: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 21:45:01 +07:00
if ( stream ) headers [ "Accept" ] = "text/event-stream" ;
return headers ;
}
2026-06-14 13:15:48 +07: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 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-06-14 13:15:48 +07:00
claude : () => this . refreshFromGrant ( credentials , proxyOptions ),
codex : () => this . refreshFromGrant ( credentials , proxyOptions ),
2026-04-28 17:28:57 +07:00
iflow : () => this . refreshIflow ( credentials . refreshToken , proxyOptions ),
2026-06-14 13:15:48 +07:00
gemini : () => this . refreshFromGrant ( credentials , proxyOptions ),
2026-04-28 17:28:57 +07:00
kiro : () => this . refreshKiro ( credentials . refreshToken , proxyOptions ),
cline : () => this . refreshCline ( credentials . refreshToken , proxyOptions ),
2026-07-03 10:50:32 +07:00
clinepass : () => this . refreshCline ( credentials . refreshToken , proxyOptions ),
2026-07-17 12:09:14 +07:00
kimi : () => this . refreshKimi ( credentials , proxyOptions ),
"kimi-coding" : () => this . refreshKimi ( credentials , proxyOptions ),
2026-04-28 17:28:57 +07:00
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 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-06-13 20:33:38 +07:00
const response = await proxyAwareFetch ( PROVIDERS . cline . refreshUrl , {
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-06-13 22:00:57 +07:00
if ( ! response . ok ) return null ;
2026-02-20 17:05:46 +07: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-03 10:50:32 +07:00
let accessToken = data ? . accessToken ;
if ( accessToken && ! accessToken . startsWith ( "workos:" )) {
accessToken = `workos: ${ accessToken } ` ;
}
return { accessToken , refreshToken : data ? . refreshToken || refreshToken , expiresIn };
2026-02-20 17:05:46 +07:00
}
2026-07-17 12:09:14 +07: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 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-07-17 12:09:14 +07:00
body : new URLSearchParams ({ grant_type : "refresh_token" , refresh_token : refreshToken , client_id : cfg . clientId })
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 ;