api9nin / open-sse /executors /default.js
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
15.3 kB
import { BaseExecutor } from "./base.js";
import { PROVIDERS, PROVIDER_OAUTH } from "../config/providers.js";
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE } from "../providers/shared.js";
import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js";
import { buildClineHeaders } from "../shared/clineAuth.js";
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js";
// 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 = {
kimiHeaders: (h) => Object.assign(h, buildKimiHeaders()),
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 },
}];
})
);
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") {
// quirk: some openai-compatible providers reject Anthropic's client_metadata field
if (this.config.quirks?.dropClientMetadata) {
delete transformed.client_metadata;
}
stripUnsupportedParams(this.provider, model, transformed);
}
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" } };
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
if (this.provider?.startsWith?.("openai-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || OPENAI_COMPAT_BASE;
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 || ANTHROPIC_COMPAT_BASE;
const normalized = baseUrl.replace(/\/$/, "");
return `${normalized}/messages`;
}
// 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);
}
return url;
}
// 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;
}
buildHeaders(credentials, stream = true) {
const headers = { "Content-Type": "application/json", ...this.config.headers };
const desc = AUTH_DESCRIPTORS[this.provider] || this.resolveAuthDescriptor();
// 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);
// 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) {
// 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}`;
}
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];
}
}
}
}
}
if (stream) headers["Accept"] = "text/event-stream";
return headers;
}
// 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);
}
async refreshCredentials(credentials, log, proxyOptions = null) {
if (!credentials.refreshToken) return null;
const refreshers = {
claude: () => this.refreshFromGrant(credentials, proxyOptions),
codex: () => this.refreshFromGrant(credentials, 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.refreshFromGrant(credentials, 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 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) {
const response = await proxyAwareFetch(PROVIDERS.cline.refreshUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension" })
}, proxyOptions);
if (!response.ok) return null;
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 };
}
async refreshKimiCoding(refreshToken, proxyOptions = null) {
const kimiHeaders = buildKimiHeaders();
const response = await proxyAwareFetch(PROVIDERS["kimi-coding"].refreshUrl, {
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: PROVIDERS["kimi-coding"].clientId })
}, 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) {
// Kilocode uses device code flow, no refresh token support
return null;
}
}
export default DefaultExecutor;