feat: cherry-pick PR #183 — multi-provider support, PWA, dynamic models, UI improvements
Cherry-picked from decolua/9router PR #183. Note: open-sse changes included but need further review due to extensive modifications. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { platform, arch } from "os";
|
||||
|
||||
// === GitHub Copilot Version Constants ===
|
||||
export const GITHUB_COPILOT = {
|
||||
VSCODE_VERSION: "1.110.0",
|
||||
COPILOT_CHAT_VERSION: "0.38.0",
|
||||
USER_AGENT: "GitHubCopilotChat/0.38.0",
|
||||
API_VERSION: "2025-04-01",
|
||||
};
|
||||
|
||||
// === Antigravity Binary Alignment: Numeric Enums ===
|
||||
// Reference: Antigravity binary analysis - google.internal.cloud.code.v1internal.ClientMetadata
|
||||
|
||||
@@ -53,7 +61,7 @@ export function getPlatformEnum() {
|
||||
export function getPlatformUserAgent() {
|
||||
const os = platform();
|
||||
const architecture = arch();
|
||||
return `antigravity/1.16.5 ${os}/${architecture}`;
|
||||
return `antigravity/1.104.0 ${os}/${architecture}`;
|
||||
}
|
||||
|
||||
// Centralized client metadata (used in request bodies for loadCodeAssist, onboardUser, etc.)
|
||||
@@ -71,7 +79,8 @@ export const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local
|
||||
export const ANTIGRAVITY_HEADERS = {
|
||||
"X-Client-Name": "antigravity",
|
||||
"X-Client-Version": "1.107.0",
|
||||
"x-goog-api-client": "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x"
|
||||
"x-goog-api-client": "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x",
|
||||
"User-Agent": "antigravity/1.107.0 darwin/arm64"
|
||||
};
|
||||
|
||||
// Cloud Code Assist API endpoints (for Project ID discovery)
|
||||
@@ -220,11 +229,11 @@ export const PROVIDERS = {
|
||||
format: "openai", // GitHub Copilot uses OpenAI-compatible format
|
||||
headers: {
|
||||
"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",
|
||||
"editor-version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`,
|
||||
"editor-plugin-version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`,
|
||||
"user-agent": GITHUB_COPILOT.USER_AGENT,
|
||||
"openai-intent": "conversation-panel",
|
||||
"x-github-api-version": "2025-04-01",
|
||||
"x-github-api-version": GITHUB_COPILOT.API_VERSION,
|
||||
"x-vscode-user-agent-library-version": "electron-fetch",
|
||||
"X-Initiator": "user",
|
||||
"Accept": "application/json",
|
||||
@@ -283,6 +292,82 @@ export const PROVIDERS = {
|
||||
},
|
||||
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
|
||||
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh"
|
||||
},
|
||||
nvidia: {
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
anthropic: {
|
||||
baseUrl: "https://api.anthropic.com/v1/messages",
|
||||
format: "claude",
|
||||
headers: {
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
|
||||
}
|
||||
},
|
||||
deepseek: {
|
||||
baseUrl: "https://api.deepseek.com/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
groq: {
|
||||
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
xai: {
|
||||
baseUrl: "https://api.x.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
mistral: {
|
||||
baseUrl: "https://api.mistral.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
perplexity: {
|
||||
baseUrl: "https://api.perplexity.ai/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
together: {
|
||||
baseUrl: "https://api.together.xyz/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
fireworks: {
|
||||
baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
cerebras: {
|
||||
baseUrl: "https://api.cerebras.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
cohere: {
|
||||
baseUrl: "https://api.cohere.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
nebius: {
|
||||
baseUrl: "https://api.studio.nebius.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
siliconflow: {
|
||||
baseUrl: "https://api.siliconflow.cn/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
hyperbolic: {
|
||||
baseUrl: "https://api.hyperbolic.xyz/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
deepgram: {
|
||||
baseUrl: "https://api.deepgram.com/v1/listen",
|
||||
format: "openai"
|
||||
},
|
||||
assemblyai: {
|
||||
baseUrl: "https://api.assemblyai.com/v1/audio/transcriptions",
|
||||
format: "openai"
|
||||
},
|
||||
nanobanana: {
|
||||
baseUrl: "https://api.nanobananaapi.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
chutes: {
|
||||
baseUrl: "https://llm.chutes.ai/v1/chat/completions",
|
||||
format: "openai"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -34,9 +34,6 @@ export const PROVIDER_MODELS = {
|
||||
gc: [ // Gemini CLI
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
|
||||
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
||||
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
||||
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
|
||||
],
|
||||
qw: [ // Qwen Code
|
||||
// { id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
|
||||
|
||||
@@ -21,11 +21,11 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${credentials.accessToken}`,
|
||||
"User-Agent": this.config.headers?.["User-Agent"] || "antigravity/1.104.0 darwin/arm64",
|
||||
"User-Agent": this.config.headers?.["User-Agent"] || ANTIGRAVITY_HEADERS['User-Agent'],
|
||||
[INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value,
|
||||
...ANTIGRAVITY_HEADERS,
|
||||
...(sessionId && { "X-Machine-Session-Id": sessionId }),
|
||||
...(stream && { "Accept": "text/event-stream" })
|
||||
"Accept": stream ? "text/event-stream" : "application/json"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS, GITHUB_COPILOT } from "../config/constants.js";
|
||||
import { openaiToOpenAIResponsesRequest } from "../translator/request/openai-responses.js";
|
||||
import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-responses.js";
|
||||
import { initState } from "../translator/index.js";
|
||||
@@ -22,11 +22,11 @@ export class GithubExecutor extends BaseExecutor {
|
||||
"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",
|
||||
"editor-version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`,
|
||||
"editor-plugin-version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`,
|
||||
"user-agent": GITHUB_COPILOT.USER_AGENT,
|
||||
"openai-intent": "conversation-panel",
|
||||
"x-github-api-version": "2025-04-01",
|
||||
"x-github-api-version": GITHUB_COPILOT.API_VERSION,
|
||||
"x-request-id": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
"x-vscode-user-agent-library-version": "electron-fetch",
|
||||
"X-Initiator": "user",
|
||||
@@ -46,7 +46,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
|
||||
if (result.response.status === HTTP_STATUS.BAD_REQUEST) {
|
||||
const errorBody = await result.response.clone().text();
|
||||
|
||||
|
||||
if (errorBody.includes("not accessible via the /chat/completions endpoint")) {
|
||||
log?.warn("GITHUB", `Model ${model} requires /responses. Switching...`);
|
||||
this.knownCodexModels.add(model);
|
||||
@@ -60,7 +60,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
async executeWithResponsesEndpoint({ model, body, stream, credentials, signal, log }) {
|
||||
const url = this.config.responsesUrl;
|
||||
const headers = this.buildHeaders(credentials, stream);
|
||||
|
||||
|
||||
const transformedBody = openaiToOpenAIResponsesRequest(model, body, stream, credentials);
|
||||
|
||||
log?.debug("GITHUB", "Sending translated request to /responses");
|
||||
@@ -86,7 +86,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
async transform(chunk, controller) {
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -110,13 +110,13 @@ export class GithubExecutor extends BaseExecutor {
|
||||
},
|
||||
flush(controller) {
|
||||
if (buffer.trim()) {
|
||||
const parsed = parseSSELine(buffer.trim());
|
||||
if (parsed && !parsed.done) {
|
||||
const converted = openaiResponsesToOpenAIResponse(parsed, state);
|
||||
if (converted) {
|
||||
controller.enqueue(new TextEncoder().encode(formatSSE(converted, "openai")));
|
||||
}
|
||||
}
|
||||
const parsed = parseSSELine(buffer.trim());
|
||||
if (parsed && !parsed.done) {
|
||||
const converted = openaiResponsesToOpenAIResponse(parsed, state);
|
||||
if (converted) {
|
||||
controller.enqueue(new TextEncoder().encode(formatSSE(converted, "openai")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -140,13 +140,18 @@ export class GithubExecutor extends BaseExecutor {
|
||||
const response = await fetch("https://api.github.com/copilot_internal/v2/token", {
|
||||
headers: {
|
||||
"Authorization": `token ${githubAccessToken}`,
|
||||
"User-Agent": "GithubCopilot/1.0",
|
||||
"Editor-Version": "vscode/1.100.0",
|
||||
"Editor-Plugin-Version": "copilot/1.300.0",
|
||||
"Accept": "application/json"
|
||||
"User-Agent": GITHUB_COPILOT.USER_AGENT,
|
||||
"Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`,
|
||||
"Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`,
|
||||
"Accept": "application/json",
|
||||
"x-github-api-version": GITHUB_COPILOT.API_VERSION
|
||||
}
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
log?.error?.("TOKEN", `Copilot token refresh failed: ${response.status} ${errorText}`);
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
log?.info?.("TOKEN", "Copilot token refreshed");
|
||||
return { token: data.token, expiresAt: data.expires_at };
|
||||
@@ -180,7 +185,7 @@ export class GithubExecutor extends BaseExecutor {
|
||||
|
||||
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) {
|
||||
@@ -191,18 +196,18 @@ export class GithubExecutor extends BaseExecutor {
|
||||
return githubTokens;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (copilotResult) {
|
||||
return { accessToken: credentials.accessToken, refreshToken: credentials.refreshToken, copilotToken: copilotResult.token, copilotTokenExpiresAt: copilotResult.expiresAt };
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
needsRefresh(credentials) {
|
||||
// Always refresh if no copilotToken
|
||||
if (!credentials.copilotToken) return true;
|
||||
|
||||
|
||||
if (credentials.copilotTokenExpiresAt) {
|
||||
// Handle both Unix timestamp (seconds) and ISO string
|
||||
let expiresAtMs = credentials.copilotTokenExpiresAt;
|
||||
|
||||
@@ -9,11 +9,39 @@ const ALIAS_TO_PROVIDER_ID = {
|
||||
gh: "github",
|
||||
kr: "kiro",
|
||||
cu: "cursor",
|
||||
// API Key providers (alias = id)
|
||||
// API Key providers
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
gemini: "gemini",
|
||||
openrouter: "openrouter",
|
||||
glm: "glm",
|
||||
kimi: "kimi",
|
||||
minimax: "minimax",
|
||||
"minimax-cn": "minimax-cn",
|
||||
ds: "deepseek",
|
||||
deepseek: "deepseek",
|
||||
groq: "groq",
|
||||
xai: "xai",
|
||||
mistral: "mistral",
|
||||
pplx: "perplexity",
|
||||
perplexity: "perplexity",
|
||||
together: "together",
|
||||
fireworks: "fireworks",
|
||||
cerebras: "cerebras",
|
||||
cohere: "cohere",
|
||||
nvidia: "nvidia",
|
||||
nebius: "nebius",
|
||||
siliconflow: "siliconflow",
|
||||
hyp: "hyperbolic",
|
||||
hyperbolic: "hyperbolic",
|
||||
dg: "deepgram",
|
||||
deepgram: "deepgram",
|
||||
aai: "assemblyai",
|
||||
assemblyai: "assemblyai",
|
||||
nb: "nanobanana",
|
||||
nanobanana: "nanobanana",
|
||||
ch: "chutes",
|
||||
chutes: "chutes",
|
||||
cursor: "cursor",
|
||||
};
|
||||
|
||||
@@ -42,7 +70,12 @@ export function parseModel(modelStr) {
|
||||
}
|
||||
|
||||
// Alias format (model alias, not provider alias)
|
||||
return { provider: null, model: modelStr, isAlias: true, providerAlias: null };
|
||||
return {
|
||||
provider: null,
|
||||
model: modelStr,
|
||||
isAlias: true,
|
||||
providerAlias: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,29 +84,29 @@ export function parseModel(modelStr) {
|
||||
*/
|
||||
export function resolveModelAliasFromMap(alias, aliases) {
|
||||
if (!aliases) return null;
|
||||
|
||||
|
||||
// Check if alias exists
|
||||
const resolved = aliases[alias];
|
||||
if (!resolved) return null;
|
||||
|
||||
|
||||
// Resolved value is "provider/model" format
|
||||
if (typeof resolved === "string" && resolved.includes("/")) {
|
||||
const firstSlash = resolved.indexOf("/");
|
||||
const providerOrAlias = resolved.slice(0, firstSlash);
|
||||
return {
|
||||
provider: resolveProviderAlias(providerOrAlias),
|
||||
model: resolved.slice(firstSlash + 1)
|
||||
model: resolved.slice(firstSlash + 1),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Or object { provider, model }
|
||||
if (typeof resolved === "object" && resolved.provider && resolved.model) {
|
||||
return {
|
||||
provider: resolveProviderAlias(resolved.provider),
|
||||
model: resolved.model
|
||||
model: resolved.model,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -93,9 +126,10 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
|
||||
}
|
||||
|
||||
// Get aliases (from object or function)
|
||||
const aliases = typeof aliasesOrGetter === "function"
|
||||
? await aliasesOrGetter()
|
||||
: aliasesOrGetter;
|
||||
const aliases =
|
||||
typeof aliasesOrGetter === "function"
|
||||
? await aliasesOrGetter()
|
||||
: aliasesOrGetter;
|
||||
|
||||
// Resolve alias
|
||||
const resolved = resolveModelAliasFromMap(parsed.model, aliases);
|
||||
@@ -103,10 +137,26 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Fallback: treat as openai model
|
||||
// Fallback: infer provider from model name prefix
|
||||
return {
|
||||
provider: "openai",
|
||||
model: parsed.model
|
||||
provider: inferProviderFromModelName(parsed.model),
|
||||
model: parsed.model,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer provider from model name prefix
|
||||
* Used as fallback when no provider prefix or alias is given
|
||||
*/
|
||||
function inferProviderFromModelName(modelName) {
|
||||
if (!modelName) return "openai";
|
||||
const m = modelName.toLowerCase();
|
||||
if (m.startsWith("claude-")) return "anthropic";
|
||||
if (m.startsWith("gemini-")) return "gemini";
|
||||
if (m.startsWith("gpt-")) return "openai";
|
||||
if (m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4"))
|
||||
return "openai";
|
||||
if (m.startsWith("deepseek-")) return "openrouter";
|
||||
// Default fallback
|
||||
return "openai";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../config/constants.js";
|
||||
|
||||
// Token expiry buffer (refresh if expires within 5 minutes)
|
||||
export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
||||
@@ -301,6 +301,7 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log)
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "kiro-cli/1.0.0",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: refreshToken,
|
||||
@@ -425,10 +426,11 @@ export async function refreshCopilotToken(githubAccessToken, log) {
|
||||
const response = await fetch("https://api.github.com/copilot_internal/v2/token", {
|
||||
headers: {
|
||||
"Authorization": `token ${githubAccessToken}`,
|
||||
"User-Agent": "GithubCopilot/1.0",
|
||||
"Editor-Version": "vscode/1.100.0",
|
||||
"Editor-Plugin-Version": "copilot/1.300.0",
|
||||
"Accept": "application/json"
|
||||
"User-Agent": GITHUB_COPILOT.USER_AGENT,
|
||||
"Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`,
|
||||
"Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`,
|
||||
"Accept": "application/json",
|
||||
"x-github-api-version": GITHUB_COPILOT.API_VERSION
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -221,16 +221,26 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"User-Agent": ANTIGRAVITY_CONFIG.userAgent,
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Name": "antigravity",
|
||||
"X-Client-Version": "1.107.0",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(projectId ? { project: projectId } : {}),
|
||||
metadata: CLIENT_METADATA,
|
||||
mode: 1
|
||||
...(projectId ? { project: projectId } : {})
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
return { message: "Antigravity access forbidden. Check subscription." };
|
||||
return {
|
||||
message: "Antigravity quota API access forbidden. Chat may still work.",
|
||||
quotas: {}
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
return {
|
||||
message: "Antigravity quota API authentication expired. Chat may still work.",
|
||||
quotas: {}
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -470,6 +480,15 @@ async function getKiroUsage(accessToken, providerSpecificData) {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
// Handle authentication errors gracefully
|
||||
if (response.status === 403 || response.status === 401) {
|
||||
return {
|
||||
message: "Kiro quota API authentication expired. Chat may still work.",
|
||||
quotas: {}
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Kiro API error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user