refactor: streamline provider interactions and enhance error handling
This commit is contained in:
90
open-sse/executors/antigravity.js
Normal file
90
open-sse/executors/antigravity.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import crypto from "crypto";
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
|
||||
export class AntigravityExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("antigravity", PROVIDERS.antigravity);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
const baseUrls = this.getBaseUrls();
|
||||
const baseUrl = baseUrls[urlIndex] || baseUrls[0];
|
||||
const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent";
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${credentials.accessToken}`,
|
||||
"User-Agent": this.config.headers?.["User-Agent"] || "antigravity/1.104.0 darwin/arm64",
|
||||
...(stream && { "Accept": "text/event-stream" })
|
||||
};
|
||||
}
|
||||
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
const projectId = credentials?.projectId || this.generateProjectId();
|
||||
|
||||
return {
|
||||
...body,
|
||||
project: projectId,
|
||||
model: model,
|
||||
userAgent: "antigravity",
|
||||
requestType: "agent",
|
||||
requestId: `agent-${crypto.randomUUID()}`,
|
||||
request: {
|
||||
...body.request,
|
||||
sessionId: body.request?.sessionId || this.generateSessionId(),
|
||||
safetySettings: undefined,
|
||||
toolConfig: body.request?.tools?.length > 0
|
||||
? { functionCallingConfig: { mode: "VALIDATED" } }
|
||||
: body.request?.toolConfig
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async refreshCredentials(credentials, log) {
|
||||
if (!credentials.refreshToken) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(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: credentials.refreshToken,
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const tokens = await response.json();
|
||||
log?.info?.("TOKEN", "Antigravity refreshed");
|
||||
|
||||
return {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token || credentials.refreshToken,
|
||||
expiresIn: tokens.expires_in,
|
||||
projectId: credentials.projectId
|
||||
};
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN", `Antigravity refresh error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
generateProjectId() {
|
||||
const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)];
|
||||
const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)];
|
||||
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
|
||||
}
|
||||
|
||||
generateSessionId() {
|
||||
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AntigravityExecutor;
|
||||
109
open-sse/executors/base.js
Normal file
109
open-sse/executors/base.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* BaseExecutor - Base class for provider executors
|
||||
*/
|
||||
export class BaseExecutor {
|
||||
constructor(provider, config) {
|
||||
this.provider = provider;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
getProvider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
getBaseUrls() {
|
||||
return this.config.baseUrls || (this.config.baseUrl ? [this.config.baseUrl] : []);
|
||||
}
|
||||
|
||||
getFallbackCount() {
|
||||
return this.getBaseUrls().length || 1;
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
const baseUrls = this.getBaseUrls();
|
||||
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...this.config.headers
|
||||
};
|
||||
|
||||
if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
} else if (credentials.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${credentials.apiKey}`;
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Override in subclass for provider-specific transformations
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
return body;
|
||||
}
|
||||
|
||||
shouldRetry(status, urlIndex) {
|
||||
return status === 429 && urlIndex + 1 < this.getFallbackCount();
|
||||
}
|
||||
|
||||
// Override in subclass for provider-specific refresh
|
||||
async refreshCredentials(credentials, log) {
|
||||
return null;
|
||||
}
|
||||
|
||||
needsRefresh(credentials) {
|
||||
if (!credentials.expiresAt) return false;
|
||||
const expiresAtMs = new Date(credentials.expiresAt).getTime();
|
||||
return expiresAtMs - Date.now() < 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
parseError(response, bodyText) {
|
||||
return { status: response.status, message: bodyText || `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
async execute({ model, body, stream, credentials, signal, log }) {
|
||||
const fallbackCount = this.getFallbackCount();
|
||||
let lastError = null;
|
||||
let lastStatus = 0;
|
||||
|
||||
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
||||
const url = this.buildUrl(model, stream, urlIndex);
|
||||
const headers = this.buildHeaders(credentials, stream);
|
||||
const transformedBody = this.transformRequest(model, body, stream, credentials);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(transformedBody),
|
||||
signal
|
||||
});
|
||||
|
||||
if (this.shouldRetry(response.status, urlIndex)) {
|
||||
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);
|
||||
lastStatus = response.status;
|
||||
continue;
|
||||
}
|
||||
|
||||
return { response, url, headers, transformedBody };
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (urlIndex + 1 < fallbackCount) {
|
||||
log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseExecutor;
|
||||
116
open-sse/executors/default.js
Normal file
116
open-sse/executors/default.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
|
||||
export class DefaultExecutor extends BaseExecutor {
|
||||
constructor(provider) {
|
||||
super(provider, PROVIDERS[provider] || PROVIDERS.openai);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
switch (this.provider) {
|
||||
case "claude":
|
||||
case "glm":
|
||||
case "kimi":
|
||||
case "minimax":
|
||||
return `${this.config.baseUrl}?beta=true`;
|
||||
case "gemini":
|
||||
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
||||
default:
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
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":
|
||||
credentials.apiKey ? headers["x-api-key"] = credentials.apiKey : headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
break;
|
||||
case "glm":
|
||||
case "kimi":
|
||||
case "minimax":
|
||||
headers["x-api-key"] = credentials.apiKey;
|
||||
break;
|
||||
default:
|
||||
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
||||
}
|
||||
|
||||
if (stream) headers["Accept"] = "text/event-stream";
|
||||
return headers;
|
||||
}
|
||||
|
||||
async refreshCredentials(credentials, log) {
|
||||
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 }),
|
||||
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" }),
|
||||
qwen: () => this.refreshWithForm(OAUTH_ENDPOINTS.qwen.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.qwen.clientId }),
|
||||
iflow: () => this.refreshIflow(credentials.refreshToken),
|
||||
gemini: () => this.refreshGoogle(credentials.refreshToken)
|
||||
};
|
||||
|
||||
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) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
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) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" },
|
||||
body: new URLSearchParams(params)
|
||||
});
|
||||
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) {
|
||||
const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`);
|
||||
const response = await fetch(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 })
|
||||
});
|
||||
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) {
|
||||
const response = await fetch(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 })
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const tokens = await response.json();
|
||||
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
|
||||
}
|
||||
}
|
||||
|
||||
export default DefaultExecutor;
|
||||
62
open-sse/executors/gemini-cli.js
Normal file
62
open-sse/executors/gemini-cli.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
|
||||
export class GeminiCLIExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("gemini-cli", PROVIDERS["gemini-cli"]);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
||||
return `${this.config.baseUrl}:${action}`;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${credentials.accessToken}`,
|
||||
...(stream && { "Accept": "text/event-stream" })
|
||||
};
|
||||
}
|
||||
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
if (!body.project && credentials?.projectId) {
|
||||
body.project = credentials.projectId;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async refreshCredentials(credentials, log) {
|
||||
if (!credentials.refreshToken) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(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: credentials.refreshToken,
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const tokens = await response.json();
|
||||
log?.info?.("TOKEN", "Gemini CLI refreshed");
|
||||
|
||||
return {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token || credentials.refreshToken,
|
||||
expiresIn: tokens.expires_in,
|
||||
projectId: credentials.projectId
|
||||
};
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN", `Gemini CLI refresh error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GeminiCLIExecutor;
|
||||
97
open-sse/executors/github.js
Normal file
97
open-sse/executors/github.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
|
||||
export class GithubExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("github", PROVIDERS.github);
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
const token = credentials.copilotToken || credentials.accessToken;
|
||||
return {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
"copilot-integration-id": "vscode-chat",
|
||||
"editor-version": "vscode/1.107.1",
|
||||
"editor-plugin-version": "copilot-chat/0.26.7",
|
||||
"user-agent": "GitHubCopilotChat/0.26.7",
|
||||
"openai-intent": "conversation-panel",
|
||||
"x-github-api-version": "2025-04-01",
|
||||
"x-request-id": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
"x-vscode-user-agent-library-version": "electron-fetch",
|
||||
"X-Initiator": "user",
|
||||
"Accept": stream ? "text/event-stream" : "application/json"
|
||||
};
|
||||
}
|
||||
|
||||
async refreshCopilotToken(githubAccessToken, log) {
|
||||
try {
|
||||
const response = await fetch("https://api.github.com/copilot_internal/v2/token", {
|
||||
headers: { "Authorization": `Bearer ${githubAccessToken}`, "User-Agent": "GitHub-Copilot/1.0", "Accept": "*/*" }
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
log?.info?.("TOKEN", "Copilot token refreshed");
|
||||
return { token: data.token, expiresAt: data.expires_at };
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN", `Copilot refresh error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshGitHubToken(refreshToken, log) {
|
||||
try {
|
||||
const response = await fetch(OAUTH_ENDPOINTS.github.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
|
||||
})
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const tokens = await response.json();
|
||||
log?.info?.("TOKEN", "GitHub token refreshed");
|
||||
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN", `GitHub refresh error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshCredentials(credentials, log) {
|
||||
let copilotResult = await this.refreshCopilotToken(credentials.accessToken, log);
|
||||
|
||||
if (!copilotResult && credentials.refreshToken) {
|
||||
const githubTokens = await this.refreshGitHubToken(credentials.refreshToken, log);
|
||||
if (githubTokens?.accessToken) {
|
||||
copilotResult = await this.refreshCopilotToken(githubTokens.accessToken, log);
|
||||
if (copilotResult) {
|
||||
return { ...githubTokens, copilotToken: copilotResult.token, copilotTokenExpiresAt: copilotResult.expiresAt };
|
||||
}
|
||||
return githubTokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (copilotResult) {
|
||||
return { accessToken: credentials.accessToken, refreshToken: credentials.refreshToken, copilotToken: copilotResult.token, copilotTokenExpiresAt: copilotResult.expiresAt };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
needsRefresh(credentials) {
|
||||
if (credentials.copilotTokenExpiresAt) {
|
||||
if (new Date(credentials.copilotTokenExpiresAt).getTime() - Date.now() < 5 * 60 * 1000) return true;
|
||||
}
|
||||
return super.needsRefresh(credentials);
|
||||
}
|
||||
}
|
||||
|
||||
export default GithubExecutor;
|
||||
28
open-sse/executors/index.js
Normal file
28
open-sse/executors/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AntigravityExecutor } from "./antigravity.js";
|
||||
import { GeminiCLIExecutor } from "./gemini-cli.js";
|
||||
import { GithubExecutor } from "./github.js";
|
||||
import { DefaultExecutor } from "./default.js";
|
||||
|
||||
const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
"gemini-cli": new GeminiCLIExecutor(),
|
||||
github: new GithubExecutor()
|
||||
};
|
||||
|
||||
const defaultCache = new Map();
|
||||
|
||||
export function getExecutor(provider) {
|
||||
if (executors[provider]) return executors[provider];
|
||||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||||
return defaultCache.get(provider);
|
||||
}
|
||||
|
||||
export function hasSpecializedExecutor(provider) {
|
||||
return !!executors[provider];
|
||||
}
|
||||
|
||||
export { BaseExecutor } from "./base.js";
|
||||
export { AntigravityExecutor } from "./antigravity.js";
|
||||
export { GeminiCLIExecutor } from "./gemini-cli.js";
|
||||
export { GithubExecutor } from "./github.js";
|
||||
export { DefaultExecutor } from "./default.js";
|
||||
Reference in New Issue
Block a user