diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index c161b749..a0261dc7 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -17,6 +17,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'minimax-cn-coding-plan', name: 'MiniMax Coding Plan (minimaxi.com)' }, { id: 'minimax-coding-plan', name: 'MiniMax Coding Plan (minimax.io)' }, { id: 'ollama-cloud', name: 'Ollama Cloud' }, + { id: 'zhipuai-coding-plan', name: 'ZhipuAI' }, ]; export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce< diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index a2e64de4..91ea729d 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -11,7 +11,8 @@ export type QuotaProviderId = | 'zai-coding-plan' | 'minimax-coding-plan' | 'minimax-cn-coding-plan' - | 'ollama-cloud'; + | 'ollama-cloud' + | 'zhipuai-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 228bfa8c..78e14e0e 100644 --- a/packages/web/server/lib/quota/DOCUMENTATION.md +++ b/packages/web/server/lib/quota/DOCUMENTATION.md @@ -29,6 +29,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide | `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` | | `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Cookie file at `~/.config/ollama-quota/cookie` (raw session cookie string) | +| `zhipuai-coding-plan` | ZhipuAI | `providers/zhipuai.js` | `zhipuai-coding-plan`, `zhipuai`, `zhipu` | ## 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 926f1185..9a171cc9 100644 --- a/packages/web/server/lib/quota/index.js +++ b/packages/web/server/lib/quota/index.js @@ -20,5 +20,6 @@ export { fetchNanoGptQuota, fetchMinimaxCodingPlanQuota, fetchMinimaxCnCodingPlanQuota, - fetchOllamaCloudQuota + fetchOllamaCloudQuota, + fetchZhipuaiQuota } 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 33311b5a..370cecd6 100644 --- a/packages/web/server/lib/quota/providers/index.js +++ b/packages/web/server/lib/quota/providers/index.js @@ -19,6 +19,7 @@ import * as zai from './zai.js'; import * as minimaxCodingPlan from './minimax-coding-plan.js'; import * as minimaxCnCodingPlan from './minimax-cn-coding-plan.js'; import * as ollamaCloud from './ollama-cloud.js'; +import * as zhipuai from './zhipuai.js'; const registry = { claude: { @@ -92,6 +93,12 @@ const registry = { providerName: ollamaCloud.providerName, isConfigured: ollamaCloud.isConfigured, fetchQuota: ollamaCloud.fetchQuota + }, + 'zhipuai-coding-plan': { + providerId: zhipuai.providerId, + providerName: zhipuai.providerName, + isConfigured: zhipuai.isConfigured, + fetchQuota: zhipuai.fetchQuota } }; @@ -150,3 +157,4 @@ export const fetchNanoGptQuota = nanogpt.fetchQuota; export const fetchMinimaxCodingPlanQuota = minimaxCodingPlan.fetchQuota; export const fetchMinimaxCnCodingPlanQuota = minimaxCnCodingPlan.fetchQuota; export const fetchOllamaCloudQuota = ollamaCloud.fetchQuota; +export const fetchZhipuaiQuota = zhipuai.fetchQuota; diff --git a/packages/web/server/lib/quota/providers/zhipuai.js b/packages/web/server/lib/quota/providers/zhipuai.js new file mode 100644 index 00000000..fe236bd0 --- /dev/null +++ b/packages/web/server/lib/quota/providers/zhipuai.js @@ -0,0 +1,114 @@ +import { readAuthFile } from '../../opencode/auth.js'; +import { readConfigLayers } from '../../opencode/shared.js'; +import { + getAuthEntry, + normalizeAuthEntry, + buildResult, + toUsageWindow, + toNumber, + toTimestamp, + resolveWindowSeconds, + resolveWindowLabel, + normalizeTimestamp +} from '../utils/index.js'; + +export const providerId = 'zhipuai-coding-plan'; +export const providerName = 'ZhipuAI'; +export const aliases = ['zhipuai-coding-plan', 'zhipuai', 'zhipu']; + +function getApiKey() { + const auth = readAuthFile(); + const oldEntry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + const apiKeyFromOld = oldEntry?.key ?? oldEntry?.token; + + if (apiKeyFromOld) { + return apiKeyFromOld; + } + + try { + const layers = readConfigLayers(); + const { mergedConfig } = layers; + + for (const alias of aliases) { + const providerConfig = mergedConfig?.provider?.[alias]; + if (providerConfig?.options?.apiKey) { + return providerConfig.options.apiKey; + } + } + } catch (error) { + // Ignore read errors + } + + return null; +} + +export const isConfigured = () => { + return Boolean(getApiKey()); +}; + +export const fetchQuota = async () => { + const apiKey = getApiKey(); + + if (!apiKey) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured' + }); + } + + try { + const response = await fetch('https://open.bigmodel.cn/api/monitor/usage/quota/limit', { + 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 limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : []; + const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT'); + const windowSeconds = resolveWindowSeconds(tokensLimit); + const windowLabel = resolveWindowLabel(windowSeconds); + const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null; + const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null; + + const windows = {}; + if (tokensLimit) { + windows[windowLabel] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt + }); + } + + 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' + }); + } +};