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:
Quan
2026-02-25 11:40:50 +07:00
committed by decolua
parent 147fc168f9
commit 07717bad60
30 changed files with 773 additions and 159 deletions

View File

@@ -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";
}

View File

@@ -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
}
});

View File

@@ -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}`);
}