Skip to content
Closed
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
3 changes: 2 additions & 1 deletion scripts/verify/verify-provider-registry-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const frontendConsumers = [
'src/lib/shareCard.ts',
];
const providerLiteral =
/["'](?:claude|codex|cursor|antigravity|copilot|devin|grok|opencode|openrouter|zai)["']/;
/["'](?:claude|codex|cursor|antigravity|copilot|devin|grok|opencode|openrouter|zai|minimax)["']/;

for (const file of rustConsumers) {
const source = fs.readFileSync(new URL(file, root), 'utf8').split('#[cfg(test)]')[0];
Expand Down Expand Up @@ -90,6 +90,7 @@ const expectedRuntimeOrder = [
'OpenCodeProvider',
'OpenRouterProvider',
'ZaiProvider',
'MiniMaxProvider',
];
if (runtimeOrder.join(',') !== expectedRuntimeOrder.join(',')) {
throw new Error(
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ use crate::{
antigravity::AntigravityProvider, claude, codex::reset_claim::CodexResetClaimService,
codex::CodexProvider, copilot::CopilotProvider, cursor::CursorProvider,
detect_local_credentials, devin::DevinProvider, grok::GrokProvider,
opencode::OpenCodeProvider, openrouter::OpenRouterProvider, zai::ZaiProvider,
ProviderRegistry, UsageProvider,
minimax::MiniMaxProvider, opencode::OpenCodeProvider, openrouter::OpenRouterProvider,
zai::ZaiProvider, ProviderRegistry, UsageProvider,
},
storage::Storage,
window::{
Expand Down Expand Up @@ -240,6 +240,7 @@ pub fn run() {
Arc::new(OpenCodeProvider::new(pricing.clone())) as Arc<dyn UsageProvider>,
Arc::new(OpenRouterProvider::new()?) as Arc<dyn UsageProvider>,
Arc::new(ZaiProvider::new()?) as Arc<dyn UsageProvider>,
Arc::new(MiniMaxProvider::new()?) as Arc<dyn UsageProvider>,
]);
let registry = Arc::new(ProviderRegistry::new(providers)?);
let (settings_service, credential_detection_plan) =
Expand Down
63 changes: 63 additions & 0 deletions src-tauri/src/providers/minimax/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::{
models::ApiKeyStatus,
providers::api_key::{ApiKeyStore, SecretString},
};

use super::MiniMaxError;

const CONFIG_PATHS: &[&str] = &["~/.config/openquota/minimax.json"];
const ENVIRONMENT_NAMES: &[&str] = &["MINIMAX_API_KEY", "MINIMAXI_API_KEY"];

#[derive(Clone)]
pub struct MiniMaxAuthStore {
store: ApiKeyStore,
}

impl MiniMaxAuthStore {
pub fn new() -> Self {
Self {
store: ApiKeyStore::new_with_sources("minimax", ENVIRONMENT_NAMES, CONFIG_PATHS),
}
}

#[cfg(test)]
pub(super) fn with_store(store: ApiKeyStore) -> Self {
Self { store }
}

pub fn load(&self) -> Result<Option<SecretString>, MiniMaxError> {
self.store.load().map_err(|_| MiniMaxError::CredentialStorage)
}

pub fn has_local_credentials(&self) -> bool {
self.load().is_ok_and(|secret| secret.is_some())
}

pub fn status(&self) -> Result<ApiKeyStatus, MiniMaxError> {
self.store.status().map_err(|_| MiniMaxError::CredentialStorage)
}

pub fn save(&self, value: &str) -> Result<(), MiniMaxError> {
self.store.save(value).map_err(|_| {
if value.trim().is_empty() {
MiniMaxError::MissingKey
} else {
crate::app_warn!("auth:minimax", "system credential store write failed");
MiniMaxError::CredentialStorage
}
})
}

pub fn delete(&self) -> Result<(), MiniMaxError> {
self.store.delete().map_err(|_| {
crate::app_warn!("auth:minimax", "system credential store delete failed");
MiniMaxError::CredentialStorage
})
}
}

impl Default for MiniMaxAuthStore {
fn default() -> Self {
Self::new()
}
}
72 changes: 72 additions & 0 deletions src-tauri/src/providers/minimax/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::time::Duration;

use reqwest::{blocking::Client, StatusCode};
use serde_json::Value;

use super::MiniMaxError;

const REMAINS_URL: &str = "https://www.minimax.io/v1/token_plan/remains";

#[derive(Debug)]
pub struct EndpointResponse {
pub status: StatusCode,
pub body: Value,
}

pub struct MiniMaxClient {
client: Client,
url: String,
}

impl MiniMaxClient {
pub fn new() -> Result<Self, MiniMaxError> {
Self::with_endpoint(REMAINS_URL, Duration::from_secs(15))
}

fn with_endpoint(url: &str, timeout: Duration) -> Result<Self, MiniMaxError> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(8))
.timeout(timeout)
.user_agent(concat!("OpenQuota/", env!("CARGO_PKG_VERSION")))
.build()
.map_err(|_| MiniMaxError::ConnectionFailed)?;
Ok(Self {
client,
url: url.to_owned(),
})
}

pub fn fetch(&self, api_key: &str) -> Result<EndpointResponse, MiniMaxError> {
let started = std::time::Instant::now();
let response = self
.client
.get(&self.url)
.bearer_auth(api_key)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.send()
.map_err(|_| {
crate::app_warn!("http", "minimax token_plan request failed (transport)");
MiniMaxError::ConnectionFailed
})?;
let status = response.status();
crate::app_debug!(
"http",
"minimax token_plan HTTP {} ({}ms)",
status.as_u16(),
started.elapsed().as_millis()
);
let text = response
.text()
.map_err(|_| MiniMaxError::InvalidResponse)?;
let body = serde_json::from_str(&text).unwrap_or(Value::Null);
Ok(EndpointResponse { status, body })
}
}

#[cfg(test)]
impl MiniMaxClient {
pub fn for_test(url: &str, timeout: Duration) -> Self {
Self::with_endpoint(url, timeout).unwrap()
}
}
Loading