diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index a0261dc7..bcb942d1 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -14,6 +14,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'nano-gpt', name: 'NanoGPT' }, { id: 'openrouter', name: 'OpenRouter' }, { id: 'zai-coding-plan', name: 'z.ai' }, + { id: 'zhipuai-coding-plan', name: 'Zhipu AI Coding Plan' }, { 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' }, diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 91ea729d..1ba803bb 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -9,6 +9,7 @@ export type QuotaProviderId = | 'nano-gpt' | 'openrouter' | 'zai-coding-plan' + | 'zhipuai-coding-plan' | 'minimax-coding-plan' | 'minimax-cn-coding-plan' | 'ollama-cloud' diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index 6d5c2aef..fd051e73 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -71,6 +71,36 @@ type ZaiPayload = { }; }; +type ZhipuaiTokensLimit = { + type: 'TOKENS_LIMIT'; + unit?: number; + number?: number; + nextResetTime?: number; + percentage?: number; +}; + +type ZhipuaiMcpTimeLimit = { + type: 'TIME_LIMIT'; + unit?: number; + number?: number; + usage?: number; + currentValue?: number; + remaining?: number; + percentage?: number; + nextResetTime?: number; + usageDetails?: Array<{ + modelCode?: string; + usage?: number; + }>; +}; + +type ZhipuaiPayload = { + data?: { + limits?: Array; + level?: string; + }; +}; + export type ProviderResult = { providerId: string; providerName: string; @@ -355,6 +385,11 @@ export const listConfiguredQuotaProviders = () => { configured.add('zai-coding-plan'); } + const zhipuaiAuth = normalizeAuthEntry(getAuthEntry(auth, ['zhipuai-coding-plan'])); + if (zhipuaiAuth && ((zhipuaiAuth as Record).key || (zhipuaiAuth as Record).token)) { + configured.add('zhipuai-coding-plan'); + } + const kimiAuth = normalizeAuthEntry(getAuthEntry(auth, ['kimi-for-coding', 'kimi'])); if (kimiAuth && ((kimiAuth as Record).key || (kimiAuth as Record).token)) { configured.add('kimi-for-coding'); @@ -1259,6 +1294,93 @@ export const fetchZaiQuota = async (): Promise => { } }; +export const fetchZhipuaiCodingPlanQuota = async (): Promise => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, ['zhipuai-coding-plan'])) as Record | null; + const apiKey = (entry?.key as string | undefined) ?? (entry?.token as string | undefined); + + if (!apiKey) { + return buildResult({ + providerId: 'zhipuai-coding-plan', + providerName: 'Zhipu AI Coding Plan', + 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: 'zhipuai-coding-plan', + providerName: 'Zhipu AI Coding Plan', + ok: false, + configured: true, + error: `API error: ${response.status}`, + }); + } + + const payload = await response.json() as ZhipuaiPayload; + const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : []; + + const tokensLimit = limits.find((limit): limit is ZhipuaiTokensLimit => limit?.type === 'TOKENS_LIMIT'); + const mcpToolsTimeLimit = limits.find((limit): limit is ZhipuaiMcpTimeLimit => limit?.type === 'TIME_LIMIT'); + + const windows: Record = {}; + + // Handle TOKENS_LIMIT (5-hour window for token usage) + if (tokensLimit) { + const windowSeconds = resolveWindowSeconds(tokensLimit); + const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null; + const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null; + + windows['Tokens'] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt, + }); + } + + // Handle TIME_LIMIT (MCP tools monthly window) + if (mcpToolsTimeLimit) { + // TIME_LIMIT unit=5 means 1 month (30 days) + const monthSeconds = 30 * 24 * 60 * 60; + const resetAt = mcpToolsTimeLimit?.nextResetTime ? normalizeTimestamp(mcpToolsTimeLimit.nextResetTime) : null; + const usedPercent = typeof mcpToolsTimeLimit?.percentage === 'number' ? mcpToolsTimeLimit.percentage : null; + + windows['MCP Tools'] = toUsageWindow({ + usedPercent, + windowSeconds: monthSeconds, + resetAt, + }); + } + + return buildResult({ + providerId: 'zhipuai-coding-plan', + providerName: 'Zhipu AI Coding Plan', + ok: true, + configured: true, + usage: { windows }, + }); + } catch (error) { + return buildResult({ + providerId: 'zhipuai-coding-plan', + providerName: 'Zhipu AI Coding Plan', + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed', + }); + } +}; + const NANO_GPT_DAILY_WINDOW_SECONDS = 86400; export const fetchNanoGptQuota = async (): Promise => { @@ -1384,6 +1506,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise} [usageDetails] + */ +import { readAuthFile } from '../../opencode/auth.js'; +import { + getAuthEntry, + normalizeAuthEntry, + buildResult, + toUsageWindow, + resolveWindowSeconds, + normalizeTimestamp +} from '../utils/index.js'; + +export const providerId = 'zhipuai-coding-plan'; +export const providerName = 'Zhipu AI Coding Plan'; +export const aliases = ['zhipuai-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://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 mcpToolsTimeLimit = limits.find((limit) => limit?.type === 'TIME_LIMIT'); + + const windows = {}; + + // Handle TOKENS_LIMIT (5-hour window for token usage) + if (tokensLimit) { + const windowSeconds = resolveWindowSeconds(tokensLimit); + const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null; + const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null; + + windows['Tokens'] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt + }); + } + + // Handle TIME_LIMIT (MCP tools monthly window) + if (mcpToolsTimeLimit) { + // TIME_LIMIT unit=5 means 1 month (30 days) + const monthSeconds = 30 * 24 * 60 * 60; + const resetAt = mcpToolsTimeLimit?.nextResetTime ? normalizeTimestamp(mcpToolsTimeLimit.nextResetTime) : null; + const usedPercent = typeof mcpToolsTimeLimit?.percentage === 'number' ? mcpToolsTimeLimit.percentage : null; + + windows['MCP Tools'] = toUsageWindow({ + usedPercent, + windowSeconds: monthSeconds, + 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' + }); + } +};