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:
@@ -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