| import { BaseExecutor } from "./base.js"; |
| import { PROVIDERS } from "../config/providers.js"; |
| import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js"; |
| import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js"; |
| import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js"; |
| import { proxyAwareFetch } from "../utils/proxyFetch.js"; |
| import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; |
|
|
| export class DefaultExecutor extends BaseExecutor { |
| constructor(provider) { |
| super(provider, PROVIDERS[provider] || PROVIDERS.openai); |
| } |
|
|
| transformRequest(model, body) { |
| const transformed = this.applyJsonSchemaFallback(body); |
|
|
| if (transformed && typeof transformed === "object") { |
| if (this.provider === "cerebras" || this.provider === "mistral") { |
| delete transformed.client_metadata; |
| } |
| } |
|
|
| return injectReasoningContent({ provider: this.provider, model, body: transformed }); |
| } |
|
|
| |
| 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" } }; |
| } |
|
|
| 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}`; |
| } |
| if (this.provider?.startsWith?.("anthropic-compatible-")) { |
| const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1"; |
| const normalized = baseUrl.replace(/\/$/, ""); |
| return `${normalized}/messages`; |
| } |
| switch (this.provider) { |
| case "claude": |
| case "glm": |
| case "kimi": |
| case "minimax": |
| case "minimax-cn": |
| return `${this.config.baseUrl}?beta=true`; |
| case "kimi-coding": |
| return `${this.config.baseUrl}?beta=true`; |
| case "gemini": |
| return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; |
| 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; |
| } |
| } |
| } |
|
|
| 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; |
| case "claude": { |
| |
| |
| const cached = getCachedClaudeHeaders(); |
| if (cached) { |
| |
| for (const lcKey of Object.keys(cached)) { |
| |
| const titleKey = lcKey.replace(/(^|-)([a-z])/g, (_, sep, c) => sep + c.toUpperCase()); |
|
|
| |
| 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)); |
|
|
| |
| 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}`); |
| break; |
| } |
| case "glm": |
| case "kimi": |
| case "minimax": |
| case "minimax-cn": |
| case "kimi-coding": |
| headers["x-api-key"] = credentials.apiKey || credentials.accessToken; |
| if (this.provider === "kimi-coding") Object.assign(headers, buildKimiHeaders()); |
| break; |
| default: |
| 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"; |
| } |
| } else if (this.provider === "gitlab") { |
| |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| } else if (this.provider === "codebuddy") { |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| } else if (this.provider === "kilocode") { |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| if (credentials.providerSpecificData?.orgId) { |
| headers["X-Kilocode-OrganizationID"] = credentials.providerSpecificData.orgId; |
| } |
| } else if (this.provider === "cline") { |
| Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken)); |
| } else if (this.config?.format === "claude") { |
| |
| headers["x-api-key"] = credentials.apiKey || credentials.accessToken; |
| if (!headers["anthropic-version"]) headers["anthropic-version"] = "2023-06-01"; |
| } else { |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| } |
| } |
|
|
| |
| if (this.provider?.startsWith?.("anthropic-compatible-")) { |
| const baseUrl = credentials?.providerSpecificData?.baseUrl || ""; |
| const isOfficialAnthropic = baseUrl === "" || baseUrl.includes("api.anthropic.com"); |
| if (!isOfficialAnthropic) { |
| |
| |
| |
| if (credentials.apiKey && !headers["Authorization"]) { |
| headers["Authorization"] = `Bearer ${credentials.apiKey}`; |
| } |
| delete headers["anthropic-dangerous-direct-browser-access"]; |
| delete headers["Anthropic-Dangerous-Direct-Browser-Access"]; |
| delete headers["x-app"]; |
| delete headers["X-App"]; |
| |
| 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]; |
| } |
| } |
| } |
| } |
| } |
|
|
| if (stream) headers["Accept"] = "text/event-stream"; |
| return headers; |
| } |
|
|
| async refreshCredentials(credentials, log, proxyOptions = null) { |
| if (!credentials.refreshToken) return null; |
|
|
| const refreshers = { |
| 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) |
| }; |
|
|
| 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; |
| } |
| } |
|
|
| async refreshWithJSON(url, body, proxyOptions = null) { |
| const response = await proxyAwareFetch(url, { |
| method: "POST", |
| headers: { "Content-Type": "application/json", "Accept": "application/json" }, |
| body: JSON.stringify(body) |
| }, proxyOptions); |
| 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 }; |
| } |
|
|
| async refreshWithForm(url, params, proxyOptions = null) { |
| const response = await proxyAwareFetch(url, { |
| method: "POST", |
| headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, |
| body: new URLSearchParams(params) |
| }, proxyOptions); |
| 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 }; |
| } |
|
|
| async refreshIflow(refreshToken, proxyOptions = null) { |
| const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); |
| const response = await proxyAwareFetch(OAUTH_ENDPOINTS.iflow.token, { |
| 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 }) |
| }, proxyOptions); |
| if (!response.ok) return null; |
| const tokens = await response.json(); |
| return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; |
| } |
|
|
| async refreshGoogle(refreshToken, proxyOptions = null) { |
| const response = await proxyAwareFetch(OAUTH_ENDPOINTS.google.token, { |
| 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 }) |
| }, proxyOptions); |
| if (!response.ok) return null; |
| const tokens = await response.json(); |
| return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; |
| } |
|
|
| async refreshKiro(refreshToken, proxyOptions = null) { |
| const response = await proxyAwareFetch(PROVIDERS.kiro.tokenUrl, { |
| method: "POST", |
| headers: { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "kiro-cli/1.0.0" }, |
| body: JSON.stringify({ refreshToken }) |
| }, proxyOptions); |
| if (!response.ok) return null; |
| const tokens = await response.json(); |
| return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn }; |
| } |
|
|
| async refreshCline(refreshToken, proxyOptions = null) { |
| console.log('[DEBUG] Refreshing Cline token, refreshToken length:', refreshToken?.length); |
| const response = await proxyAwareFetch("https://api.cline.bot/api/v1/auth/refresh", { |
| method: "POST", |
| headers: { "Content-Type": "application/json", "Accept": "application/json" }, |
| body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" }) |
| }, proxyOptions); |
| 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 }; |
| } |
|
|
| async refreshKimiCoding(refreshToken, proxyOptions = null) { |
| const kimiHeaders = buildKimiHeaders(); |
| const response = await proxyAwareFetch("https://auth.kimi.com/api/oauth/token", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/x-www-form-urlencoded", |
| "Accept": "application/json", |
| ...kimiHeaders |
| }, |
| body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: "17e5f671-d194-4dfb-9706-5516cb48c098" }) |
| }, proxyOptions); |
| if (!response.ok) return null; |
| const tokens = await response.json(); |
| return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; |
| } |
|
|
| async refreshKilocode(refreshToken, proxyOptions = null) { |
| |
| return null; |
| } |
| } |
|
|
| export default DefaultExecutor; |
|
|