Skip to content
Open
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ An [OpenCode](https://opencode.ai) plugin to query account quota usage for multi
| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` |
| Zhipu AI | Coding Plan | `~/.local/share/opencode/auth.json` |
| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` |
| Synthetic.new | API Key | `~/.local/share/opencode/auth.json` |
| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` |
| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` |

Expand Down Expand Up @@ -130,6 +131,25 @@ Account: 9c89****AQVM (Z.ai)
Used: 0.5M / 10.0M
Resets in: 4h

## Synthetic Account Quota

Account: Synthetic (API)

5-hour limit
██████████████████████████████ 99% remaining
Used: 28 / 750
Quota resets: 48m (2026-04-27T03:34:13.000Z)

Weekly limit
██████████████████████████████ 99% remaining
Used: $35.85 / $36.00
Quota resets: 1h 32m (2026-04-27T04:18:36.000Z)

Search limit
██████████████████████████████ 100% remaining
Used: 0 / 250
Quota resets: 1h 39m (2026-04-27T04:25:14.238Z)

## GitHub Copilot Account Quota

Account: GitHub Copilot (individual)
Expand Down Expand Up @@ -161,7 +181,7 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░

No additional configuration required. The plugin automatically reads credentials from:

- **OpenAI, Zhipu AI, Z.ai & GitHub Copilot**: `~/.local/share/opencode/auth.json`
- **OpenAI, Zhipu AI, Z.ai, Synthetic.new & GitHub Copilot**: `~/.local/share/opencode/auth.json`
- **Google Cloud**: `~/.config/opencode/antigravity-accounts.json`

### Google Cloud Setup
Expand All @@ -182,6 +202,7 @@ This plugin is safe to use:
- `https://chatgpt.com/backend-api/wham/usage` - OpenAI official quota API
- `https://bigmodel.cn/api/monitor/usage/quota/limit` - Zhipu AI official quota API
- `https://api.z.ai/api/monitor/usage/quota/limit` - Z.ai official quota API
- `https://api.synthetic.new/v2/quotas` - Synthetic.new official quota API
- `https://api.github.com/copilot_internal/user` - GitHub Copilot official API
- `https://oauth2.googleapis.com/token` - Google official OAuth API
- `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` - Google Cloud official API
Expand Down
23 changes: 22 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| OpenAI | Plus / Team / Pro | `~/.local/share/opencode/auth.json` |
| 智谱 AI | Coding Plan | `~/.local/share/opencode/auth.json` |
| Z.ai | Coding Plan | `~/.local/share/opencode/auth.json` |
| Synthetic.new | API Key | `~/.local/share/opencode/auth.json` |
| GitHub Copilot | Individual / Business | `~/.local/share/opencode/auth.json` |
| Google Cloud | Antigravity | `~/.config/opencode/antigravity-accounts.json` |

Expand Down Expand Up @@ -130,6 +131,25 @@ Account: 9c89****AQVM (Z.ai)
已用: 0.5M / 10.0M
重置: 4小时后

## Synthetic 账号额度

Account: Synthetic (API)

5 小时限额
██████████████████████████████ 剩余 99%
已用: 28 / 750
配额重置: 48分钟 (2026-04-27T03:34:13.000Z)

周额度
██████████████████████████████ 剩余 99%
已用: $35.85 / $36.00
配额重置: 1小时32分钟 (2026-04-27T04:18:36.000Z)

搜索额度
██████████████████████████████ 剩余 100%
已用: 0 / 250
配额重置: 1小时39分钟 (2026-04-27T04:25:14.238Z)

## GitHub Copilot Account Quota

Account: GitHub Copilot (individual)
Expand Down Expand Up @@ -161,7 +181,7 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░

无需额外配置。插件自动从以下位置读取认证信息:

- **OpenAI、智谱 AI、Z.ai 和 GitHub Copilot**: `~/.local/share/opencode/auth.json`
- **OpenAI、智谱 AI、Z.ai、Synthetic.new 和 GitHub Copilot**: `~/.local/share/opencode/auth.json`
- **Google Cloud**: `~/.config/opencode/antigravity-accounts.json`

### Google Cloud 设置
Expand All @@ -182,6 +202,7 @@ Claude 2d 9h ░░░░░░░░░░░░░░░░░░░
- `https://chatgpt.com/backend-api/wham/usage` - OpenAI 官方额度查询接口
- `https://bigmodel.cn/api/monitor/usage/quota/limit` - 智谱 AI 官方额度查询接口
- `https://api.z.ai/api/monitor/usage/quota/limit` - Z.ai 官方额度查询接口
- `https://api.synthetic.new/v2/quotas` - Synthetic.new 官方额度查询接口
- `https://api.github.com/copilot_internal/user` - GitHub Copilot 官方 API
- `https://oauth2.googleapis.com/token` - Google 官方 OAuth 接口
- `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` - Google Cloud 官方接口
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-mystatus",
"version": "1.2.4",
"description": "Check all your AI subscription quotas in one command. Supports OpenAI, Zhipu AI, Z.ai, and Google Antigravity. More platforms coming soon.",
"description": "Check all your AI subscription quotas in one command. Supports OpenAI, Zhipu AI, Z.ai, Synthetic.new, GitHub Copilot, and Google Antigravity.",
"type": "module",
"main": "dist/plugin/mystatus.js",
"types": "dist/plugin/mystatus.d.ts",
Expand Down
16 changes: 14 additions & 2 deletions plugin/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,27 @@ const translations = {
tokenExpired:
"⚠️ OAuth 授权已过期,请在 OpenCode 中使用一次 OpenAI 模型以刷新授权。",
noAccounts:
"未找到任何已配置的账号。\n\n支持的账号类型:\n- OpenAI (Plus/Team/Pro 订阅用户)\n- 智谱 AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Google Cloud (Antigravity)",
"未找到任何已配置的账号。\n\n支持的账号类型:\n- OpenAI (Plus/Team/Pro 订阅用户)\n- 智谱 AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Synthetic.new (API Key)\n- Google Cloud (Antigravity)",
queryFailed: "❌ 查询失败的账号:\n",

// 平台标题
openaiTitle: "## OpenAI 账号额度",
zhipuTitle: "## 智谱 AI 账号额度",
zaiTitle: "## Z.ai 账号额度",
syntheticTitle: "## Synthetic 账号额度",

// 智谱 AI 相关
zhipuApiError: (status: number, text: string) =>
`智谱 API 请求失败 (${status}): ${text}`,
zaiApiError: (status: number, text: string) =>
`Z.ai API 请求失败 (${status}): ${text}`,
syntheticApiError: (status: number, text: string) =>
`Synthetic API 请求失败 (${status}): ${text}`,
zhipuTokensLimit: "5 小时 Token 限额",
zhipuMcpLimit: "MCP 月度配额",
syntheticFiveHourLimit: "5 小时限额",
syntheticWeeklyLimit: "周额度",
syntheticSearchLimit: "搜索额度",
zhipuAccountName: "Coding Plan",
zaiAccountName: "Z.ai",
noQuotaData: "暂无配额数据",
Expand Down Expand Up @@ -149,21 +155,27 @@ const translations = {
tokenExpired:
"⚠️ OAuth token expired. Please use an OpenAI model in OpenCode to refresh authorization.",
noAccounts:
"No configured accounts found.\n\nSupported account types:\n- OpenAI (Plus/Team/Pro subscribers)\n- Zhipu AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Google Cloud (Antigravity)",
"No configured accounts found.\n\nSupported account types:\n- OpenAI (Plus/Team/Pro subscribers)\n- Zhipu AI (Coding Plan)\n- Z.ai (Coding Plan)\n- Synthetic.new (API Key)\n- Google Cloud (Antigravity)",
queryFailed: "❌ Failed to query accounts:\n",

// 平台标题
openaiTitle: "## OpenAI Account Quota",
zhipuTitle: "## Zhipu AI Account Quota",
zaiTitle: "## Z.ai Account Quota",
syntheticTitle: "## Synthetic Account Quota",

// 智谱 AI 相关
zhipuApiError: (status: number, text: string) =>
`Zhipu API request failed (${status}): ${text}`,
zaiApiError: (status: number, text: string) =>
`Z.ai API request failed (${status}): ${text}`,
syntheticApiError: (status: number, text: string) =>
`Synthetic API request failed (${status}): ${text}`,
zhipuTokensLimit: "5-hour token limit",
zhipuMcpLimit: "MCP monthly quota",
syntheticFiveHourLimit: "5-hour limit",
syntheticWeeklyLimit: "Weekly limit",
syntheticSearchLimit: "Search limit",
zhipuAccountName: "Coding Plan",
zaiAccountName: "Z.ai",
noQuotaData: "No quota data available",
Expand Down
199 changes: 199 additions & 0 deletions plugin/lib/synthetic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Synthetic.new 额度查询模块
*
* [输入]: API Key
* [输出]: 格式化的额度使用情况
* [定位]: 被 mystatus.ts 调用,处理 Synthetic 账号
* [同步]: mystatus.ts, types.ts, utils.ts, i18n.ts
*/

import { t } from "./i18n";
import {
type QueryResult,
type ZhipuAuthData,
HIGH_USAGE_THRESHOLD,
} from "./types";
import { formatDuration, createProgressBar, fetchWithTimeout } from "./utils";

interface SyntheticQuotaResponse {
search?: {
hourly?: {
limit: number;
requests: number;
renewsAt: string;
};
};
weeklyTokenLimit?: {
nextRegenAt: string;
percentRemaining: number;
maxCredits: string;
remainingCredits: string;
nextRegenCredits: string;
};
rollingFiveHourLimit?: {
remaining: number;
max: number;
nextTickAt: string;
};
}

interface QuotaItem {
label: string;
usedText: string;
remainPercent: number;
renewsAt: string;
}

interface CountQuota {
limit: number;
requests: number;
renewsAt: string;
}

const SYNTHETIC_QUOTA_QUERY_URL = "https://api.synthetic.new/v2/quotas";

async function fetchSyntheticUsage(
apiKey: string,
): Promise<SyntheticQuotaResponse> {
const response = await fetchWithTimeout(SYNTHETIC_QUOTA_QUERY_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"User-Agent": "OpenCode-Status-Plugin/1.0",
},
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(t.syntheticApiError(response.status, errorText));
}

return response.json() as Promise<SyntheticQuotaResponse>;
}

function isValidCountQuota(
quota: CountQuota | undefined,
): quota is CountQuota {
return !!quota && typeof quota.limit === "number" && typeof quota.requests === "number";
}

function calcRemainPercent(limit: number, used: number): number {
if (limit <= 0) return 0;
const remain = Math.max(0, limit - used);
const rawPercent = (remain / limit) * 100;
let percent = Math.floor(rawPercent);

// Avoid displaying 100% when there is any usage.
if (used > 0 && remain > 0 && percent >= 100) {
percent = 99;
}

return Math.max(0, Math.min(100, percent));
}

function formatSyntheticUsage(data: SyntheticQuotaResponse): string {
const lines: string[] = [];
const quotas: QuotaItem[] = [];

if (data.rollingFiveHourLimit) {
const limit = Math.max(0, data.rollingFiveHourLimit.max);
const used = Math.max(0, limit - data.rollingFiveHourLimit.remaining);
const remainPercent = calcRemainPercent(limit, used);

quotas.push({
label: t.syntheticFiveHourLimit,
usedText: `${used} / ${limit}`,
remainPercent,
renewsAt: data.rollingFiveHourLimit.nextTickAt,
});
}

if (data.weeklyTokenLimit) {
const weekly = data.weeklyTokenLimit;
const rawPercent = Math.max(0, Math.min(100, weekly.percentRemaining));
const hasUsage = weekly.remainingCredits !== weekly.maxCredits;
const remainPercent =
hasUsage && rawPercent >= 100 ? 99 : Math.floor(rawPercent);

quotas.push({
label: t.syntheticWeeklyLimit,
usedText: `${weekly.remainingCredits} / ${weekly.maxCredits}`,
remainPercent,
renewsAt: weekly.nextRegenAt,
});
}

if (isValidCountQuota(data.search?.hourly)) {
const limit = Math.max(0, data.search.hourly.limit);
const used = Math.max(0, data.search.hourly.requests);
const remainPercent = calcRemainPercent(limit, used);

quotas.push({
label: t.syntheticSearchLimit,
usedText: `${used} / ${limit}`,
remainPercent,
renewsAt: data.search.hourly.renewsAt,
});
}

if (quotas.length === 0) {
lines.push(`${t.account} Synthetic (API)`);
lines.push("");
lines.push(t.noQuotaData);
return lines.join("\n");
}

lines.push(`${t.account} Synthetic (API)`);
lines.push("");

for (const [index, quota] of quotas.entries()) {
const progressBar = createProgressBar(quota.remainPercent);

if (index > 0) lines.push("");
lines.push(quota.label);
lines.push(`${progressBar} ${t.remaining(quota.remainPercent)}`);
lines.push(`${t.used}: ${quota.usedText}`);

const renewAtDate = new Date(quota.renewsAt);
if (!Number.isNaN(renewAtDate.getTime())) {
const resetSeconds = Math.max(
0,
Math.floor((renewAtDate.getTime() - Date.now()) / 1000),
);
lines.push(
`${t.quotaResets}: ${formatDuration(resetSeconds)} (${renewAtDate.toISOString()})`,
);
} else {
lines.push(`${t.quotaResets}: ${quota.renewsAt}`);
}

if (quota.remainPercent <= 100 - HIGH_USAGE_THRESHOLD) {
lines.push("");
lines.push(t.limitReached);
}
}

return lines.join("\n");
}

export async function querySyntheticUsage(
authData: ZhipuAuthData | undefined,
): Promise<QueryResult | null> {
if (!authData || authData.type !== "api" || !authData.key) {
return null;
}

try {
const usage = await fetchSyntheticUsage(authData.key);
return {
success: true,
output: formatSyntheticUsage(usage),
};
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
}
1 change: 1 addition & 0 deletions plugin/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export interface AuthData {
openai?: OpenAIAuthData;
"zhipuai-coding-plan"?: ZhipuAuthData;
"zai-coding-plan"?: ZhipuAuthData;
synthetic?: ZhipuAuthData;
"github-copilot"?: CopilotAuthData;
}

Expand Down
Loading