Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
954 changes: 497 additions & 457 deletions docs/i18n-inventory.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub struct AppConfig {
pub anthropic_api: ApiCostConfig,
pub openai_api: ApiCostConfig,
pub nan: NanConfig,
pub minimax: MiniMaxConfig,
pub nine_router: NineRouterConfig,
/// Cuentas adicionales de un proveedor (la principal es la de arriba).
pub accounts: Vec<Account>,
Expand Down Expand Up @@ -295,6 +296,7 @@ impl Default for AppConfig {
anthropic_api: ApiCostConfig::default(),
openai_api: ApiCostConfig::default(),
nan: NanConfig::default(),
minimax: MiniMaxConfig::default(),
nine_router: NineRouterConfig::default(),
accounts: vec![],
}
Expand Down Expand Up @@ -425,6 +427,28 @@ pub struct ApiCostConfig {
pub monthly_budget_usd: Option<f64>,
}

/// MiniMax. `base_url` cubre las dos regiones: global `api.minimax.io` y
/// China `api.minimaxi.com`.
///
/// Verificado contra la API en los cuatro hosts: `api.` y `www.` de ambas
/// regiones responden el mismo JSON con el sobre `base_resp`, así que cualquiera
/// de los dos sirve. Se usa `api.` por semántica (es el host de la API, no el
/// sitio web). La documentación oficial del Token Plan muestra `www.minimax.io`
/// en su ejemplo de `curl`, y ese host también responde, por si alguien prefiere
/// copiarlo tal cual desde Ajustes.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default, rename_all = "camelCase")]
pub struct MiniMaxConfig {
pub enabled: bool,
pub base_url: String,
}

impl Default for MiniMaxConfig {
fn default() -> Self {
Self { enabled: false, base_url: "https://api.minimax.io".into() }
}
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default, rename_all = "camelCase")]
pub struct NanConfig {
Expand Down Expand Up @@ -523,6 +547,10 @@ impl AppConfig {
self.nan.base_url = NanConfig::default().base_url;
}
self.nan.base_url = self.nan.base_url.trim().trim_end_matches('/').to_string();
if self.minimax.base_url.trim().is_empty() {
self.minimax.base_url = MiniMaxConfig::default().base_url;
}
self.minimax.base_url = self.minimax.base_url.trim().trim_end_matches('/').to_string();
self.nan.billing_day = self.nan.billing_day.clamp(1, 28);
if self.nine_router.base_url.trim().is_empty() {
self.nine_router.base_url = NineRouterConfig::default().base_url;
Expand Down
362 changes: 362 additions & 0 deletions src-tauri/src/providers/minimax.rs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src-tauri/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod copilot;
pub mod cursor;
pub mod gemini;
pub mod nan;
pub mod minimax;
pub mod nan_cloud;
pub mod nine_router;
pub mod nine_router_creds;
Expand Down Expand Up @@ -83,6 +84,9 @@ pub async fn fetch_all(cfg: &AppConfig, http: &reqwest::Client) -> Vec<UsageRepo
if cfg.openrouter.enabled {
jobs.push(async move { vec![openrouter::fetch(&cfg.openrouter, Who::main("openrouter"), http).await] }.boxed());
}
if cfg.minimax.enabled {
jobs.push(async move { vec![minimax::fetch(&cfg.minimax, Who::main("minimax"), http).await] }.boxed());
}
if cfg.nine_router.enabled {
// 9Router lleva su propio registro de uso; ademas del orbe agregado,
// descubrimos sus sub-proveedores y los exponemos como cuentas propias.
Expand Down Expand Up @@ -121,6 +125,7 @@ pub async fn fetch_one(cfg: &AppConfig, id: &str, http: &reqwest::Client) -> Opt
"copilot" => copilot::fetch(&cfg.copilot, Who::main("copilot"), http).await,
"nan" => nan::fetch(&cfg.nan, Who::main("nan"), http).await,
"openrouter" => openrouter::fetch(&cfg.openrouter, Who::main("openrouter"), http).await,
"minimax" => minimax::fetch(&cfg.minimax, Who::main("minimax"), http).await,
"nine_router" => nine_router::fetch(&cfg.nine_router, Who::main("nine_router"), http).await,
"anthropic_api" => anthropic_api::fetch(&cfg.anthropic_api, Who::main("anthropic_api"), http).await,
"openai_api" => openai_api::fetch(&cfg.openai_api, Who::main("openai_api"), http).await,
Expand Down
10 changes: 9 additions & 1 deletion src-tauri/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pub const NAN_API_KEY: &str = "nan_api_key";
pub const NAN_SESSION: &str = "nan_session";

pub const OPENROUTER_API_KEY: &str = "openrouter_api_key";
/// Subscription Key del plan M/Token de MiniMax (endpoint `token_plan/remains`).
pub const MINIMAX_SUBSCRIPTION_KEY: &str = "minimax_subscription_key";
/// API key de pago por uso de MiniMax (endpoint `query_balance`). Son credenciales
/// distintas en la consola: esta no da acceso al plan y la anterior no lee el saldo.
pub const MINIMAX_API_KEY: &str = "minimax_api_key";
/// Token fine-grained de GitHub con permiso «Plan» (lectura) para la facturación de Copilot.
pub const GITHUB_TOKEN: &str = "github_token";
/// Token de Cursor pegado a mano (si no, se lee de la app).
Expand All @@ -25,12 +30,14 @@ pub const NINE_ROUTER_SESSION: &str = "nine_router_session";
/// coincide con el destino actual, no se envían (evita credenciales de A en B).
pub const NINE_ROUTER_BOUND_URL: &str = "nine_router_bound_url";

pub const ALL: [&str; 10] = [
pub const ALL: [&str; 12] = [
ANTHROPIC_ADMIN_KEY,
OPENAI_ADMIN_KEY,
NAN_API_KEY,
NAN_SESSION,
OPENROUTER_API_KEY,
MINIMAX_SUBSCRIPTION_KEY,
MINIMAX_API_KEY,
GITHUB_TOKEN,
CURSOR_TOKEN,
NINE_ROUTER_PASSWORD,
Expand All @@ -44,6 +51,7 @@ pub fn for_provider(provider: &str) -> &'static [&'static str] {
"cursor" => &[CURSOR_TOKEN],
"copilot" => &[GITHUB_TOKEN],
"openrouter" => &[OPENROUTER_API_KEY],
"minimax" => &[MINIMAX_SUBSCRIPTION_KEY, MINIMAX_API_KEY],
"anthropic_api" => &[ANTHROPIC_ADMIN_KEY],
"openai_api" => &[OPENAI_ADMIN_KEY],
"nan" => &[NAN_API_KEY, NAN_SESSION],
Expand Down
52 changes: 52 additions & 0 deletions src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const LINKS = {
nanDocs: 'https://nan.builders/docs',
githubToken: 'https://github.com/settings/personal-access-tokens/new',
openrouterKeys: 'https://openrouter.ai/settings/keys',
minimaxKeys: 'https://www.minimax.io/',
minimaxKeysCn: 'https://www.minimaxi.com/',
geminiCli: 'https://github.com/google-gemini/gemini-cli',
releases: 'https://github.com/egdev6/quorb/releases',
repo: 'https://github.com/egdev6/quorb',
Expand Down Expand Up @@ -1042,6 +1044,7 @@ const PROVIDER_TABS = [
{ id: 'gemini', name: 'Gemini', group: 'sub', desc: 'Cuota diaria por modelo' },
{ id: 'cursor', name: 'Cursor', group: 'sub', desc: 'Uso del plan en el ciclo y on-demand' },
{ id: 'copilot', name: 'GitHub Copilot', group: 'sub', desc: 'Peticiones premium del mes' },
{ id: 'minimax', name: 'MiniMax', group: 'sub', desc: 'Cuota del plan M/Token y saldo de pago por uso' },
{ id: 'nan', name: 'NaN Builders', group: 'api', desc: 'Cuota del plan y tokens por día' },
{ id: 'openrouter', name: 'OpenRouter', group: 'api', desc: 'Saldo y gasto por clave' },
{ id: 'anthropic_api', name: 'Anthropic API', group: 'api', desc: 'Gasto del mes con clave de admin' },
Expand Down Expand Up @@ -1113,6 +1116,9 @@ function mainState(id: ProviderTab, cfg: AppConfig, s: SecretStatus): ProviderSt
return on(cfg.nan.enabled, !!s.nan_session || !!s.nan_api_key);
case 'openrouter':
return on(cfg.openrouter.enabled, !!s.openrouter_api_key);
case 'minimax':
// Solo la Subscription Key es obligatoria; el saldo PAYG es un extra opcional.
return on(cfg.minimax.enabled, !!s.minimax_subscription_key);
case 'anthropic_api':
return on(cfg.anthropicApi.enabled, !!s.anthropic_admin_key);
case 'openai_api':
Expand Down Expand Up @@ -1938,6 +1944,52 @@ export function Settings() {
</Section>
)}

{tab === 'minimax' && (
<Section kicker="Suscripción · API oficial" title="MiniMax">
<Toggle label="Mostrar en el widget" checked={isAccountVisible(cfg, 'minimax', cfg.minimax.enabled)} onChange={(v) => setVisibility('minimax', v)} />
<p className="q-note">
Cuota del plan y, si añades la segunda clave, tu saldo de pago por uso. Son
<b> dos credenciales distintas</b> en la consola: la primera da acceso al plan y la segunda al saldo.
</p>
<ol className="q-steps" aria-label="Cómo conectar la cuenta">
<li data-done={!!secrets.minimax_subscription_key || undefined}>
<b>Subscription Key del plan</b>
<SecretInput
id="minimax_subscription_key"
label="Subscription Key"
configured={!!secrets.minimax_subscription_key}
onSaved={loadSecrets}
hint={<>Del panel M/Token en <Link href={LINKS.minimaxKeys}>minimax.io</Link>. En la región de China, en <Link href={LINKS.minimaxKeysCn}>minimaxi.com</Link> (el host por región se cambia en Ajustes avanzados).</>}
/>
</li>
<li data-done={!!secrets.minimax_api_key || undefined}>
<b>API key de pago por uso (opcional)</b>
<SecretInput
id="minimax_api_key"
label="API key PAYG"
configured={!!secrets.minimax_api_key}
onSaved={loadSecrets}
hint={<>Sin ella solo se muestra la cuota del plan; el saldo es opcional y su fallo no lo degrade.</>}
/>
</li>
<HealthStep id="minimax" {...healthFor('minimax', !cfg.minimax.enabled)} />
</ol>
<details className="q-details">
<summary>Avanzado · URL del API</summary>
<Field
label="Base URL"
hint="Las claves son por región: global en api.minimax.io, China en api.minimaxi.com. Si el anillo sale con error 1004 o 2049, prueba con la de tu región."
>
<input
type="text"
spellCheck={false}
value={cfg.minimax.baseUrl}
onChange={(e) => set('minimax', { baseUrl: e.target.value })}
/>
</Field>
</details>
</Section>
)}
{tab === 'openrouter' && (
<Section kicker="Pago por uso · API oficial" title="OpenRouter">
<Toggle label="Mostrar en el widget" checked={isAccountVisible(cfg, 'openrouter', cfg.openrouter.enabled)} onChange={(v) => setVisibility('openrouter', v)} />
Expand Down
9 changes: 9 additions & 0 deletions src/assets/logos/minimax.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/ProviderLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const ALIASES: Record<string, string[]> = {
cursor: ['cursor'],
copilot: ['copilot', 'github-copilot', 'microsoft-copilot', 'github'],
openrouter: ['openrouter'],
// `minimax-cn` es el nombre del nodo de 9Router para la región de China.
minimax: ['minimax', 'minimax-cn', 'minimax_cn', 'minimaxi'],
nine_router: ['nine_router', '9router'],
// Nombres de nodos personalizados de 9Router → logo del proveedor real.
zhipu: ['zhipu', 'z-ai', 'glm', 'glm-cn'],
Expand Down Expand Up @@ -107,6 +109,7 @@ const MONOGRAM: Record<string, { text: string; sub?: string }> = {
cursor: { text: 'Cu' },
copilot: { text: 'Co' },
openrouter: { text: 'OR' },
minimax: { text: 'MM' },
nine_router: { text: '9R' },
};

Expand Down
4 changes: 2 additions & 2 deletions src/lib/settings-visibility.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { AppConfig } from './types';
import { isNineRouterId } from './visibility';

type MainKey = 'claude' | 'codex' | 'gemini' | 'cursor' | 'copilot' | 'nan' | 'openrouter' | 'anthropicApi' | 'openaiApi';
type MainKey = 'claude' | 'codex' | 'gemini' | 'cursor' | 'copilot' | 'nan' | 'openrouter' | 'minimax' | 'anthropicApi' | 'openaiApi';

/** Clave de configuración de cada cuenta principal; el id del informe puede ser distinto. */
export const CONFIG_KEY: Readonly<Record<string, MainKey>> = {
claude: 'claude', codex: 'codex', gemini: 'gemini', cursor: 'cursor',
copilot: 'copilot', nan: 'nan', openrouter: 'openrouter',
copilot: 'copilot', nan: 'nan', openrouter: 'openrouter', minimax: 'minimax',
anthropic_api: 'anthropicApi', openai_api: 'openaiApi',
};

Expand Down
9 changes: 9 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export interface ApiCostConfig {
monthlyBudgetUsd: number | null;
}

/** MiniMax. `baseUrl` cubre las dos regiones: global `api.minimax.io` y China `api.minimaxi.com`. */
export interface MiniMaxConfig {
enabled: boolean;
baseUrl: string;
}

export interface NanConfig {
enabled: boolean;
baseUrl: string;
Expand Down Expand Up @@ -154,6 +160,7 @@ export interface AppConfig {
anthropicApi: ApiCostConfig;
openaiApi: ApiCostConfig;
nan: NanConfig;
minimax: MiniMaxConfig;
nineRouter: NineRouterConfig;
/** Cuentas adicionales de un proveedor (la principal es la de su campo) */
accounts: Account[];
Expand Down Expand Up @@ -195,6 +202,8 @@ export type SecretKey =
| 'nan_api_key'
| 'nan_session'
| 'openrouter_api_key'
| 'minimax_subscription_key'
| 'minimax_api_key'
| 'github_token'
| 'cursor_token'
| 'nine_router_password'
Expand Down
1 change: 1 addition & 0 deletions src/lib/widget-refresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function cfg(over: Partial<AppConfig> = {}): AppConfig {
copilot: { enabled: true },
openrouter: { enabled: true },
nan: { enabled: true },
minimax: { enabled: false },
anthropicApi: { enabled: false },
openaiApi: { enabled: false },
nineRouter: { enabled: true, baseUrl: 'http://localhost:20128', period: '7d', showSubProviders: true, minSpendUsd: 0, monthlyBudgetUsd: null },
Expand Down
2 changes: 1 addition & 1 deletion src/lib/widget-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { AppConfig } from './types';
*/
export function dataKey(c: AppConfig | null): string {
return c
? JSON.stringify([c.claude, c.codex, c.gemini, c.cursor, c.copilot, c.openrouter, c.anthropicApi, c.openaiApi, c.nan, c.nineRouter])
? JSON.stringify([c.claude, c.codex, c.gemini, c.cursor, c.copilot, c.openrouter, c.anthropicApi, c.openaiApi, c.nan, c.minimax, c.nineRouter])
: '';
}

Expand Down
Loading