# v0.4.55 (2026-05-18)
## Features - Xiaomi MiMo Token Plan: region selector (Singapore / China / Europe) — keys are cluster-specific - Antigravity: risk confirmation dialog before first connection - Gemini CLI: surface upstream retry delay on 429 errors ## Fixes - MITM: cannot kill process on macOS under sudo (lsof not found in PATH) - Stream: false-positive stall timeout on Claude reasoning / Kiro responses - Tunnel: cannot re-enable after disable (stuck state) - Tunnel: cloudflared error messages now include log tail for easier debugging - Language switcher: applies selected locale immediately on close (#1234) - Antigravity OAuth: metadata now matches the official client ## Improvements - Gemini CLI: bump engine to 0.34.0 - Re-hide `qwen` (OAuth EOL) and `iflow` (not ready) providers
This commit is contained in:
parent
90a47c3f29
commit
613a0a819a
12 changed files with 125 additions and 47 deletions
24
cli/cli.js
24
cli/cli.js
|
|
@ -197,8 +197,8 @@ function killCloudflaredByAppPort(appPort) {
|
|||
function killAllAppProcesses(appPort) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// Kill MITM first (admin/sudo process, needs special handling)
|
||||
killMitmByPidFile();
|
||||
// Kill MIT first (privileged process, needs special handling)
|
||||
killProxyByPidFile();
|
||||
// Kill cloudflared/tailscale by PID file (precise, only this app's tunnel)
|
||||
killTunnelByPidFile();
|
||||
|
||||
|
|
@ -305,13 +305,13 @@ function waitForExit(pid, timeoutMs) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Kill MITM server by PID file (MITM runs as admin/sudo, needs special handling)
|
||||
// Sends SIGTERM first so MITM can clean up /etc/hosts entries before dying.
|
||||
function killMitmByPidFile() {
|
||||
// Kill MIT server by PID file (runs privileged, needs special handling)
|
||||
// Sends SIGTERM first so MIT can clean up host entries before dying.
|
||||
function killProxyByPidFile() {
|
||||
try {
|
||||
const mitmPidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid");
|
||||
if (!fs.existsSync(mitmPidFile)) return;
|
||||
const pid = parseInt(fs.readFileSync(mitmPidFile, "utf8").trim(), 10);
|
||||
const pidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid");
|
||||
if (!fs.existsSync(pidFile)) return;
|
||||
const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
|
||||
if (!pid) return;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
|
|
@ -333,7 +333,7 @@ function killMitmByPidFile() {
|
|||
catch { try { process.kill(pid, "SIGKILL"); } catch { } }
|
||||
}
|
||||
}
|
||||
try { fs.unlinkSync(mitmPidFile); } catch { }
|
||||
try { fs.unlinkSync(pidFile); } catch { }
|
||||
} catch { }
|
||||
}
|
||||
|
||||
|
|
@ -584,8 +584,8 @@ function startServer(latestVersion) {
|
|||
const { killTray } = require("./src/cli/tray/tray");
|
||||
killTray();
|
||||
} catch (e) { }
|
||||
// Kill MITM server (admin/sudo process) via PID file
|
||||
killMitmByPidFile();
|
||||
// Kill MIT server (privileged process) via PID file
|
||||
killProxyByPidFile();
|
||||
// Kill cloudflared/tailscale via PID file (only this app's tunnel)
|
||||
killTunnelByPidFile();
|
||||
// Kill server process directly
|
||||
|
|
@ -772,7 +772,7 @@ function startServer(latestVersion) {
|
|||
if (aliveMs >= RESTART_RESET_MS) restartCount = 0;
|
||||
|
||||
if (restartCount >= MAX_RESTARTS) {
|
||||
console.error(`\n⚠️ Server crashed ${MAX_RESTARTS} times. Disabling MITM and restarting...`);
|
||||
console.error(`\n⚠️ Server crashed ${MAX_RESTARTS} times. Disabling MIT and restarting...`);
|
||||
try {
|
||||
const dbPath = path.join(os.homedir(), process.platform === "win32" ? path.join("AppData", "Roaming", "9router", "db.json") : path.join(".9router", "db.json"));
|
||||
if (fs.existsSync(dbPath)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "9router",
|
||||
"version": "0.4.52",
|
||||
"version": "0.4.55",
|
||||
"description": "9Router CLI - Start and manage 9Router server",
|
||||
"bin": {
|
||||
"9router": "./cli.js"
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ export const PROVIDERS = {
|
|||
baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1/chat/completions",
|
||||
format: "openai"
|
||||
},
|
||||
// Region map for Xiaomi MiMo Token Plan (keys are cluster-specific)
|
||||
// Used by resolveXiaomiTokenplanBaseUrl below
|
||||
// === Free-tier providers (synced from OmniRoute) ===
|
||||
// Claude-format with Claude CLI header spoofing (auth: x-api-key)
|
||||
agentrouter: { baseUrl: "https://agentrouter.org/v1/messages", format: "claude", headers: { ...CLAUDE_CLI_SPOOF_HEADERS } },
|
||||
|
|
@ -437,3 +439,15 @@ export function resolveOllamaLocalHost(credentials) {
|
|||
const raw = credentials?.providerSpecificData?.baseUrl?.trim();
|
||||
return (raw || OLLAMA_LOCAL_DEFAULT_HOST).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export const XIAOMI_TOKENPLAN_REGIONS = {
|
||||
sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
|
||||
cn: "https://token-plan-cn.xiaomimimo.com/v1",
|
||||
ams: "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
};
|
||||
export const XIAOMI_TOKENPLAN_DEFAULT_REGION = "sgp";
|
||||
|
||||
export function resolveXiaomiTokenplanBaseUrl(credentials) {
|
||||
const region = credentials?.providerSpecificData?.region;
|
||||
return XIAOMI_TOKENPLAN_REGIONS[region] || XIAOMI_TOKENPLAN_REGIONS[XIAOMI_TOKENPLAN_DEFAULT_REGION];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS } from "../config/providers.js";
|
||||
import { PROVIDERS, resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
|
||||
import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js";
|
||||
import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js";
|
||||
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
|
||||
|
|
@ -39,6 +39,9 @@ export class DefaultExecutor extends BaseExecutor {
|
|||
case "gemini":
|
||||
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
||||
default: {
|
||||
if (this.provider === "xiaomi-tokenplan") {
|
||||
return `${resolveXiaomiTokenplanBaseUrl(credentials)}/chat/completions`;
|
||||
}
|
||||
const url = this.config.baseUrl;
|
||||
if (url?.includes("{accountId}")) {
|
||||
const accountId = credentials?.providerSpecificData?.accountId;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "9router-app",
|
||||
"version": "0.4.52",
|
||||
"version": "0.4.55",
|
||||
"description": "9Router web dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
|
||||
const BULK_PLACEHOLDER = `name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named`;
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
|||
|
||||
const isAzure = provider === "azure";
|
||||
const isCloudflareAi = provider === "cloudflare-ai";
|
||||
const providerRegions = AI_PROVIDERS?.[provider]?.regions || null;
|
||||
const defaultRegion = AI_PROVIDERS?.[provider]?.defaultRegion || providerRegions?.[0]?.id || "";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
|
|
@ -33,6 +36,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
|||
organization: "",
|
||||
});
|
||||
const [cloudflareData, setCloudflareData] = useState({ accountId: "" });
|
||||
const [region, setRegion] = useState(defaultRegion);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -55,6 +59,9 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
|||
if (isCloudflareAi) {
|
||||
return { accountId: cloudflareData.accountId };
|
||||
}
|
||||
if (providerRegions && region) {
|
||||
return { region };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
|
@ -234,6 +241,14 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
|||
)}
|
||||
</p>
|
||||
)}
|
||||
{providerRegions && (
|
||||
<Select
|
||||
label="Region"
|
||||
value={region}
|
||||
onChange={(e) => setRegion(e.target.value)}
|
||||
options={providerRegions.map((r) => ({ value: r.id, label: r.label }))}
|
||||
/>
|
||||
)}
|
||||
{isCompatible && (
|
||||
<Input
|
||||
label="Default Model"
|
||||
|
|
|
|||
|
|
@ -49,8 +49,40 @@ export default function ProviderDetailPage() {
|
|||
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
||||
const [disabledModelIds, setDisabledModelIds] = useState([]);
|
||||
const [confirmState, setConfirmState] = useState(null);
|
||||
const [showAgRiskModal, setShowAgRiskModal] = useState(false);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
const AG_RISK_STORAGE_KEY = "ag_risk_confirmed";
|
||||
|
||||
const triggerAddConnection = () => {
|
||||
if (providerId === "antigravity" && typeof window !== "undefined") {
|
||||
const confirmed = window.localStorage.getItem(AG_RISK_STORAGE_KEY) === "true";
|
||||
if (!confirmed) {
|
||||
setShowAgRiskModal(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isOAuth) {
|
||||
setShowOAuthModal(true);
|
||||
return;
|
||||
}
|
||||
setAddConnectionError("");
|
||||
setShowAddApiKeyModal(true);
|
||||
};
|
||||
|
||||
const handleAgRiskConfirm = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(AG_RISK_STORAGE_KEY, "true");
|
||||
}
|
||||
setShowAgRiskModal(false);
|
||||
if (isOAuth) {
|
||||
setShowOAuthModal(true);
|
||||
return;
|
||||
}
|
||||
setAddConnectionError("");
|
||||
setShowAddApiKeyModal(true);
|
||||
};
|
||||
|
||||
const providerInfo = providerNode
|
||||
? {
|
||||
id: providerNode.id,
|
||||
|
|
@ -1066,14 +1098,7 @@ export default function ProviderDetailPage() {
|
|||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => {
|
||||
if (isOAuth) {
|
||||
setShowOAuthModal(true);
|
||||
return;
|
||||
}
|
||||
setAddConnectionError("");
|
||||
setShowAddApiKeyModal(true);
|
||||
}}
|
||||
onClick={triggerAddConnection}
|
||||
>
|
||||
{isCompatible ? "Add API Key" : (providerId === "iflow" ? "OAuth" : "Add Connection")}
|
||||
</Button>
|
||||
|
|
@ -1099,14 +1124,7 @@ export default function ProviderDetailPage() {
|
|||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => {
|
||||
if (isOAuth) {
|
||||
setShowOAuthModal(true);
|
||||
return;
|
||||
}
|
||||
setAddConnectionError("");
|
||||
setShowAddApiKeyModal(true);
|
||||
}}
|
||||
onClick={triggerAddConnection}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add
|
||||
|
|
@ -1242,6 +1260,18 @@ export default function ProviderDetailPage() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* AG Risk Confirmation Modal */}
|
||||
<ConfirmModal
|
||||
isOpen={showAgRiskModal}
|
||||
onClose={() => setShowAgRiskModal(false)}
|
||||
onConfirm={handleAgRiskConfirm}
|
||||
title="Risk Notice"
|
||||
message={providerInfo?.deprecationNotice}
|
||||
confirmText="I Understand, Continue"
|
||||
cancelText="Cancel"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
{/* Confirm Modal */}
|
||||
<ConfirmModal
|
||||
isOpen={!!confirmState}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|||
import { getProviderNodeById } from "@/models";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider, AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { getDefaultModel } from "open-sse/config/providerModels.js";
|
||||
import { resolveOllamaLocalHost, PROVIDERS } from "open-sse/config/providers.js";
|
||||
import { resolveOllamaLocalHost, resolveXiaomiTokenplanBaseUrl, PROVIDERS } from "open-sse/config/providers.js";
|
||||
import { openaiToCommandCode } from "open-sse/translator/request/openai-to-commandcode.js";
|
||||
import { PROVIDER_ENDPOINTS } from "@/shared/constants/config";
|
||||
import { normalizeProviderId } from "@/lib/providerNormalization";
|
||||
|
|
@ -382,7 +382,7 @@ export async function POST(request) {
|
|||
chutes: "https://llm.chutes.ai/v1/models",
|
||||
nvidia: "https://integrate.api.nvidia.com/v1/models",
|
||||
"xiaomi-mimo": "https://api.xiaomimimo.com/v1/models",
|
||||
"xiaomi-tokenplan": "https://token-plan-sgp.xiaomimimo.com/v1/models"
|
||||
"xiaomi-tokenplan": `${resolveXiaomiTokenplanBaseUrl({ providerSpecificData })}/models`
|
||||
};
|
||||
const headers = {};
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
|
|
|
|||
|
|
@ -124,7 +124,10 @@ async function canAccessPublicLlmApi(request) {
|
|||
}
|
||||
|
||||
async function canAccessLocalOnlyRoute(request) {
|
||||
return await hasValidCliToken(request);
|
||||
if (await hasValidCliToken(request)) return true;
|
||||
// Browser on host: loopback Host + Origin (blocks tunnel/CSRF) + JWT cookie (blocks unauth raw clients)
|
||||
if (isLocalRequest(request) && await hasValidToken(request)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function hasValidToken(request) {
|
||||
|
|
|
|||
|
|
@ -305,6 +305,8 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
// Keep a small tail of raw cloudflared logs to surface real failure causes
|
||||
let logTail = "";
|
||||
|
||||
function getQuickTunnelUrlFromLog(message) {
|
||||
// cloudflared logs may contain "api.trycloudflare.com" as well,
|
||||
|
|
@ -326,13 +328,14 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
|||
if (resolved) return;
|
||||
resolved = true;
|
||||
cleanup();
|
||||
reject(new Error("Quick tunnel timed out"));
|
||||
reject(new Error(`Quick tunnel timed out. Last log: ${logTail.slice(-800) || "(empty)"}`));
|
||||
}, 90000);
|
||||
|
||||
let lastUrl = null;
|
||||
|
||||
const handleLog = (data) => {
|
||||
const msg = data.toString();
|
||||
logTail = (logTail + msg).slice(-4000);
|
||||
const tunnelUrl = getQuickTunnelUrlFromLog(msg);
|
||||
if (!tunnelUrl) return;
|
||||
|
||||
|
|
@ -370,17 +373,18 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
|||
cloudflaredProcess = null;
|
||||
clearPid();
|
||||
console.log(`[Tunnel] cloudflared exit code=${code} signal=${signal}`);
|
||||
if (logTail) console.log(`[Tunnel] cloudflared log tail:\n${logTail.slice(-1500)}`);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
// Provide more helpful error messages for common exit codes
|
||||
const tail = logTail.slice(-600).trim() || "(empty)";
|
||||
if (code === 1) {
|
||||
reject(new Error(`cloudflared exited with code ${code}. This often means: (1) the tunnel token is invalid or expired, (2) network connectivity issues, or (3) cloudflared cannot reach the local server.`));
|
||||
reject(new Error(`cloudflared quick tunnel exited (code 1). Common causes: (1) outbound port 7844 (TCP/UDP) blocked, (2) TryCloudflare service issue, (3) cannot reach 127.0.0.1:${localPort}, (4) protocol (http2/quic) blocked by network. Last log: ${tail}`));
|
||||
} else if (code === 2) {
|
||||
reject(new Error(`cloudflared exited with code ${code}. Check that arguments are correct.`));
|
||||
reject(new Error(`cloudflared exited (code 2). Bad arguments. Last log: ${tail}`));
|
||||
} else {
|
||||
reject(new Error(`cloudflared exited with code ${code}`));
|
||||
reject(new Error(`cloudflared exited (code ${code}). Last log: ${tail}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from "crypto";
|
||||
import { loadState, saveState, generateShortId } from "./state.js";
|
||||
import { loadState, saveState, generateShortId, clearPid } from "./state.js";
|
||||
import { spawnQuickTunnel, killCloudflared, isCloudflaredRunning, setUnexpectedExitHandler } from "./cloudflared.js";
|
||||
import { startFunnel, stopFunnel, isTailscaleRunning, isTailscaleRunningStrict, isTailscaleLoggedIn, startLogin, startDaemonWithPassword, provisionCert } from "./tailscale.js";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
|
|
@ -127,12 +127,15 @@ export async function enableTunnel(localPort = 20128) {
|
|||
await updateSettings({ tunnelEnabled: true, tunnelUrl });
|
||||
console.log(`[Tunnel] registered shortId=${shortId} publicUrl=${publicUrl}`);
|
||||
|
||||
// Verify direct tunnel URL first (avoid CDN false positive on publicUrl)
|
||||
await waitForHealth(tunnelUrl, token);
|
||||
console.log("[Tunnel] direct URL healthy");
|
||||
// Then verify public URL (DNS propagated through worker)
|
||||
// Verify publicUrl first (worker route is reliable; direct *.trycloudflare.com DNS may lag)
|
||||
await waitForHealth(publicUrl, token);
|
||||
console.log("[Tunnel] public URL healthy");
|
||||
// Direct tunnel probe is best-effort: DNS for *.trycloudflare.com can be slow/blocked on some networks
|
||||
if (!(await probeUrlAlive(tunnelUrl))) {
|
||||
console.warn("[Tunnel] direct URL not reachable yet, continuing via publicUrl");
|
||||
} else {
|
||||
console.log("[Tunnel] direct URL healthy");
|
||||
}
|
||||
|
||||
// Prime reachable cache so UI shows correct state immediately
|
||||
tunnelReachable.value = true;
|
||||
|
|
@ -151,15 +154,21 @@ export async function enableTunnel(localPort = 20128) {
|
|||
|
||||
export async function disableTunnel() {
|
||||
console.log("[Tunnel] disable");
|
||||
// Abort any in-flight enable so it cannot resurrect state after we clear it
|
||||
tunnelSvc.cancelToken.cancelled = true;
|
||||
setUnexpectedExitHandler(null);
|
||||
killCloudflared(tunnelSvc.activeLocalPort);
|
||||
|
||||
try { killCloudflared(tunnelSvc.activeLocalPort); } catch (e) { console.warn(`[Tunnel] kill warn: ${e.message}`); }
|
||||
clearPid();
|
||||
|
||||
const state = loadState();
|
||||
if (state) saveState({ shortId: state.shortId, machineId: state.machineId, tunnelUrl: null });
|
||||
|
||||
await updateSettings({ tunnelEnabled: false, tunnelUrl: "" });
|
||||
tunnelReachable.value = false; tunnelReachable.url = null; tunnelReachable.fetchedAt = Date.now();
|
||||
// Force-clear flags so a subsequent enable is not blocked by a stuck spawnInProgress
|
||||
tunnelSvc.spawnInProgress = false;
|
||||
tunnelSvc.activeLocalPort = null;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const MINIMAX_TTS_MODELS = [
|
|||
// OAuth Providers
|
||||
export const OAUTH_PROVIDERS = {
|
||||
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757", deprecated: true, deprecationNotice: RISK_NOTICE, website: "https://claude.ai", notice: { signupUrl: "https://claude.ai" } },
|
||||
antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", deprecated: true, deprecationNotice: "AG is designed exclusively for Antigravity IDE. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans.", website: "https://antigravity.google", notice: { signupUrl: "https://antigravity.google" } },
|
||||
antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", deprecated: true, deprecationNotice: "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.", website: "https://antigravity.google", notice: { signupUrl: "https://antigravity.google" } },
|
||||
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", deprecated: true, deprecationNotice: RISK_NOTICE, thinkingConfig: THINKING_CONFIG.effort, serviceKinds: ["llm", "image"], kindNotice: { image: "Requires a ChatGPT Plus (or higher) account. Free accounts are not supported for image generation." }, website: "https://chatgpt.com/codex", notice: { signupUrl: "https://chatgpt.com/codex" } },
|
||||
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333", deprecated: true, deprecationNotice: RISK_NOTICE, serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://models.github.ai/inference/embeddings", authType: "apikey", authHeader: "bearer", models: [{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", dimensions: 1536 }, { id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", dimensions: 3072 }] }, website: "https://github.com/features/copilot", notice: { signupUrl: "https://github.com/features/copilot" } },
|
||||
cursor: { id: "cursor", alias: "cu", name: "Cursor IDE", icon: "edit_note", color: "#00D4AA", website: "https://cursor.com", notice: { signupUrl: "https://cursor.com" } },
|
||||
|
|
@ -75,7 +75,7 @@ export const APIKEY_PROVIDERS = {
|
|||
alicode: { id: "alicode", alias: "alicode", name: "Alibaba", icon: "cloud", color: "#FF6A00", textIcon: "ALi", website: "https://bailian.console.aliyun.com", notice: { apiKeyUrl: "https://bailian.console.aliyun.com/?apiKey=1" } },
|
||||
"alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi", website: "https://modelstudio.console.alibabacloud.com", notice: { apiKeyUrl: "https://modelstudio.console.alibabacloud.com/?apiKey=1" } },
|
||||
"xiaomi-mimo": { id: "xiaomi-mimo", alias: "mimo", name: "Xiaomi MiMo", icon: "smart_toy", color: "#FF6900", textIcon: "XM", website: "https://xiaomimimo.com", notice: { apiKeyUrl: "https://xiaomimimo.com" } },
|
||||
"xiaomi-tokenplan": { id: "xiaomi-tokenplan", alias: "xmtp", name: "Xiaomi MiMo (Token Plan)", icon: "smart_toy", color: "#FF6700", textIcon: "XT", website: "https://mimo.xiaomi.com", notice: { text: "Xiaomi MiMo Token Plan subscription (API key starts with tp-). Uses Singapore endpoint.", apiKeyUrl: "https://mimo.xiaomi.com" } },
|
||||
"xiaomi-tokenplan": { id: "xiaomi-tokenplan", alias: "xmtp", name: "Xiaomi MiMo (Token Plan)", icon: "smart_toy", color: "#FF6700", textIcon: "XT", website: "https://mimo.xiaomi.com", notice: { text: "Xiaomi MiMo Token Plan subscription (API key starts with tp-). Token Plan keys are cluster-specific — select the region matching your subscription.", apiKeyUrl: "https://mimo.xiaomi.com" }, hasProviderSpecificData: true, regions: [{ id: "sgp", label: "Singapore", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1" }, { id: "cn", label: "China", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1" }, { id: "ams", label: "Europe", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1" }], defaultRegion: "sgp" },
|
||||
"volcengine-ark": { id: "volcengine-ark", alias: "ark", name: "Volcengine Ark", icon: "cloud", color: "#1677FF", textIcon: "ARK", website: "https://ark.cn-beijing.volces.com", notice: { apiKeyUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey" } },
|
||||
openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com", notice: { apiKeyUrl: "https://platform.openai.com/api-keys" }, serviceKinds: ["llm", "embedding", "tts", "stt", "image", "imageToText", "webSearch"], thinkingConfig: THINKING_CONFIG.effort, searchViaChat: { defaultModel: "gpt-4o-mini", pricingUrl: "https://openai.com/api/pricing" }, ttsConfig: { baseUrl: "https://api.openai.com/v1/audio/speech", authType: "apikey", authHeader: "bearer", format: "openai", models: [{ id: "tts-1", name: "TTS-1" }, { id: "tts-1-hd", name: "TTS-1 HD" }, { id: "gpt-4o-mini-tts", name: "GPT-4o Mini TTS" }] }, sttConfig: { baseUrl: "https://api.openai.com/v1/audio/transcriptions", authType: "apikey", authHeader: "bearer", format: "openai", models: [{ id: "whisper-1", name: "Whisper 1" }, { id: "gpt-4o-transcribe", name: "GPT-4o Transcribe" }, { id: "gpt-4o-mini-transcribe", name: "GPT-4o Mini Transcribe" }] }, embeddingConfig: { baseUrl: "https://api.openai.com/v1/embeddings", authType: "apikey", authHeader: "bearer", models: [{ id: "text-embedding-3-small", name: "Text Embedding 3 Small", dimensions: 1536 }, { id: "text-embedding-3-large", name: "Text Embedding 3 Large", dimensions: 3072 }, { id: "text-embedding-ada-002", name: "Text Embedding Ada 002", dimensions: 1536 }] } },
|
||||
"vercel-ai-gateway": { id: "vercel-ai-gateway", alias: "vercel", name: "Vercel AI Gateway", icon: "deployed_code", color: "#111827", textIcon: "VG", website: "https://vercel.com/ai-gateway", notice: { text: "Unified OpenAI-compatible endpoint from Vercel. Use your AI Gateway API key, then pick models with provider/model IDs like anthropic/claude-sonnet-4.6 or openai/gpt-5.4.", apiKeyUrl: "https://vercel.com/dashboard/~/ai-gateway" }, passthroughModels: true, serviceKinds: ["llm"] },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue