diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index 3c871b64..db74e32c 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -14,12 +14,13 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'nano-gpt', name: 'NanoGPT' }, { id: 'openrouter', name: 'OpenRouter' }, { id: 'zai-coding-plan', name: 'z.ai' }, + { id: 'minimax-cn-coding-plan', name: 'MiniMax Coding Plan (minimaxi.com)' }, + { id: 'minimax-coding-plan', name: 'MiniMax Coding Plan (minimax.io)' }, ]; -export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce>( - (acc, provider) => { - acc[provider.id] = provider; - return acc; - }, - {} -); +export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce< + Record +>((acc, provider) => { + acc[provider.id] = provider; + return acc; +}, {}); diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 7d1dafbe..856ec026 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -8,7 +8,9 @@ export type QuotaProviderId = | 'kimi-for-coding' | 'nano-gpt' | 'openrouter' - | 'zai-coding-plan'; + | 'zai-coding-plan' + | 'minimax-coding-plan' + | 'minimax-cn-coding-plan'; export interface UsageWindow { usedPercent: number | null; diff --git a/packages/web/server/lib/quota/DOCUMENTATION.md b/packages/web/server/lib/quota/DOCUMENTATION.md index e5a9f72b..f7c1d941 100644 --- a/packages/web/server/lib/quota/DOCUMENTATION.md +++ b/packages/web/server/lib/quota/DOCUMENTATION.md @@ -25,6 +25,8 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide | `nano-gpt` | NanoGPT | `providers/nanogpt.js` | `nano-gpt`, `nanogpt`, `nano_gpt` | | `openrouter` | OpenRouter | `providers/openrouter.js` | `openrouter` | | `zai-coding-plan` | z.ai | `providers/zai.js` | `zai-coding-plan`, `zai`, `z.ai` | +| `minimax-coding-plan` | MiniMax Coding Plan (minimax.io) | `providers/minimax-coding-plan.js` | `minimax-coding-plan` | +| `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` | `minimax-cn-coding-plan` | ## Internal-only provider module - `providers/openai.js` exists for logic parity/reuse but is intentionally not registered for dispatcher ID routing. diff --git a/packages/web/server/lib/quota/index.js b/packages/web/server/lib/quota/index.js index 7c8e83e4..e05cbade 100644 --- a/packages/web/server/lib/quota/index.js +++ b/packages/web/server/lib/quota/index.js @@ -17,5 +17,7 @@ export { fetchKimiQuota, fetchOpenRouterQuota, fetchZaiQuota, - fetchNanoGptQuota + fetchNanoGptQuota, + fetchMinimaxCodingPlanQuota, + fetchMinimaxCnCodingPlanQuota } from './providers/index.js'; diff --git a/packages/web/server/lib/quota/providers/index.js b/packages/web/server/lib/quota/providers/index.js index 3d208475..b7f11014 100644 --- a/packages/web/server/lib/quota/providers/index.js +++ b/packages/web/server/lib/quota/providers/index.js @@ -16,6 +16,8 @@ import * as nanogpt from './nanogpt.js'; import * as openai from './openai.js'; import * as openrouter from './openrouter.js'; import * as zai from './zai.js'; +import * as minimaxCodingPlan from './minimax-coding-plan.js'; +import * as minimaxCnCodingPlan from './minimax-cn-coding-plan.js'; const registry = { claude: { @@ -71,6 +73,18 @@ const registry = { providerName: copilot.providerNameAddon, isConfigured: copilot.isConfigured, fetchQuota: copilot.fetchQuotaAddon + }, + 'minimax-coding-plan': { + providerId: minimaxCodingPlan.providerId, + providerName: minimaxCodingPlan.providerName, + isConfigured: minimaxCodingPlan.isConfigured, + fetchQuota: minimaxCodingPlan.fetchQuota + }, + 'minimax-cn-coding-plan': { + providerId: minimaxCnCodingPlan.providerId, + providerName: minimaxCnCodingPlan.providerName, + isConfigured: minimaxCnCodingPlan.isConfigured, + fetchQuota: minimaxCnCodingPlan.fetchQuota } }; @@ -126,3 +140,5 @@ export const fetchKimiQuota = kimi.fetchQuota; export const fetchOpenRouterQuota = openrouter.fetchQuota; export const fetchZaiQuota = zai.fetchQuota; export const fetchNanoGptQuota = nanogpt.fetchQuota; +export const fetchMinimaxCodingPlanQuota = minimaxCodingPlan.fetchQuota; +export const fetchMinimaxCnCodingPlanQuota = minimaxCnCodingPlan.fetchQuota; diff --git a/packages/web/server/lib/quota/providers/minimax-cn-coding-plan.js b/packages/web/server/lib/quota/providers/minimax-cn-coding-plan.js new file mode 100644 index 00000000..3102b341 --- /dev/null +++ b/packages/web/server/lib/quota/providers/minimax-cn-coding-plan.js @@ -0,0 +1,131 @@ +// MiniMax Coding Plan Provider +import { readAuthFile } from '../../opencode/auth.js'; +import { + getAuthEntry, + normalizeAuthEntry, + buildResult, + toUsageWindow, + toNumber, + toTimestamp, +} from '../utils/index.js'; + +export const providerId = 'minimax-cn-coding-plan'; +export const providerName = 'MiniMax Coding Plan (minimaxi.com)'; +export const aliases = ['minimax-cn-coding-plan']; + +export const isConfigured = () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + return Boolean(entry?.key || entry?.token); +}; + +export const fetchQuota = async () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + const apiKey = entry?.key ?? entry?.token; + + if (!apiKey) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured', + }); + } + + try { + const response = await fetch( + 'https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains', + { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: `API error: ${response.status}`, + }); + } + + const payload = await response.json(); + + const baseResp = payload?.base_resp; + if (baseResp && baseResp.status_code !== 0) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: baseResp.status_msg || `API error: ${baseResp.status_code}`, + }); + } + + const windows = {}; + const modelRemains = payload?.model_remains; + + if (Array.isArray(modelRemains) && modelRemains.length > 0) { + const firstModel = modelRemains[0]; + const total = toNumber(firstModel?.current_interval_total_count); + const used = 600 - toNumber(firstModel?.current_interval_usage_count); + + if (total === null || used === null) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: 'Missing required quota fields', + }); + } + + const usedPercent = + total > 0 ? Math.max(0, Math.min(100, (used / total) * 100)) : null; + + const startTime = toTimestamp(firstModel?.start_time); + const endTime = toTimestamp(firstModel?.end_time); + const windowSeconds = + startTime && endTime && endTime > startTime + ? Math.floor((endTime - startTime) / 1000) + : null; + + windows['5h'] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt: endTime, + }); + } else { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: 'No model quota data available', + }); + } + + return buildResult({ + providerId, + providerName, + ok: true, + configured: true, + usage: { windows }, + }); + } catch (error) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed', + }); + } +}; diff --git a/packages/web/server/lib/quota/providers/minimax-coding-plan.js b/packages/web/server/lib/quota/providers/minimax-coding-plan.js new file mode 100644 index 00000000..d9fad8ac --- /dev/null +++ b/packages/web/server/lib/quota/providers/minimax-coding-plan.js @@ -0,0 +1,131 @@ +// MiniMax Coding Plan Provider +import { readAuthFile } from '../../opencode/auth.js'; +import { + getAuthEntry, + normalizeAuthEntry, + buildResult, + toUsageWindow, + toNumber, + toTimestamp, +} from '../utils/index.js'; + +export const providerId = 'minimax-coding-plan'; +export const providerName = 'MiniMax Coding Plan (minimax.io)'; +export const aliases = ['minimax-coding-plan']; + +export const isConfigured = () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + return Boolean(entry?.key || entry?.token); +}; + +export const fetchQuota = async () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + const apiKey = entry?.key ?? entry?.token; + + if (!apiKey) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured', + }); + } + + try { + const response = await fetch( + 'https://www.minimax.io/v1/api/openplatform/coding_plan/remains', + { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: `API error: ${response.status}`, + }); + } + + const payload = await response.json(); + + const baseResp = payload?.base_resp; + if (baseResp && baseResp.status_code !== 0) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: baseResp.status_msg || `API error: ${baseResp.status_code}`, + }); + } + + const windows = {}; + const modelRemains = payload?.model_remains; + + if (Array.isArray(modelRemains) && modelRemains.length > 0) { + const firstModel = modelRemains[0]; + const total = toNumber(firstModel?.current_interval_total_count); + const used = 600-toNumber(firstModel?.current_interval_usage_count); + + if (total === null || used === null) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: 'Missing required quota fields', + }); + } + + const usedPercent = + total > 0 ? Math.max(0, Math.min(100, (used / total) * 100)) : null; + + const startTime = toTimestamp(firstModel?.start_time); + const endTime = toTimestamp(firstModel?.end_time); + const windowSeconds = + startTime && endTime && endTime > startTime + ? Math.floor((endTime - startTime) / 1000) + : null; + + windows['5h'] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt: endTime, + }); + } else { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: 'No model quota data available', + }); + } + + return buildResult({ + providerId, + providerName, + ok: true, + configured: true, + usage: { windows }, + }); + } catch (error) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed', + }); + } +};