diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..20f8408 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3b611b4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "compile", + "problemMatcher": "$tsc", + "presentation": { + "reveal": "silent" + }, + "group": "build" + } + ] +} diff --git a/changelog.md b/changelog.md index 5f3cd0a..ec32395 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,19 @@ # Changelog +## [1.0.5] - 2026-02-22 + +### Changed +- **Auto-detect Plan Tier**: Removed manual plan tier configuration - extension now automatically detects your account level (Lite/Pro/Max) from API response +- **Improved API Response Parsing**: Enhanced quota window detection and display logic + +### Added +- **Screenshots Section**: Added UI screenshots to README for better visual preview of the extension +- **API Documentation**: Added comprehensive API documentation in `docs/API-DOCUMENTATION.md` +- **Debug Configuration**: Added `.vscode/launch.json` and `.vscode/tasks.json` for easier debugging + +### Fixed +- **Status Bar Display**: Fixed status bar display issues and improved tooltip rendering + ## [1.0.4] - 2026-01-03 ### Changed diff --git a/docs/API-DOCUMENTATION.md b/docs/API-DOCUMENTATION.md new file mode 100644 index 0000000..58790c9 --- /dev/null +++ b/docs/API-DOCUMENTATION.md @@ -0,0 +1,460 @@ +# Z.AI Usage Monitoring API Documentation + +> **Status**: ✅ Verified (2026-02-21) +> **Base URL**: `https://api.z.ai` +> **API Type**: Internal/Private (from zai-org/zai-coding-plugins) + +## Authentication + +All endpoints require Bearer token authentication via the `Authorization` header: + +```http +Authorization: your-api-key-here +``` + +## Common Headers + +```http +Authorization: {API_KEY} +Content-Type: application/json +Accept-Language: en-US,en +``` + +--- + +## 1. Model Usage Query + +Query model call counts and token usage over a time range. + +### Endpoint + +``` +GET /api/monitor/usage/model-usage +``` + +### Query Parameters + +| Parameter | Type | Required | Format | Description | +| ----------- | ------ | -------- | --------------------- | ------------------------- | +| `startTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | Start time of query range | +| `endTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | End time of query range | + +### Example Request + +```http +GET /api/monitor/usage/model-usage?startTime=2026-02-20%2020%3A00%3A00&endTime=2026-02-21%2020%3A59%3A59 +Authorization: your-api-key-here +Accept-Language: en-US,en +``` + +### Example Response + +```json +{ + "code": 200, + "msg": "Operation successful", + "data": { + "x_time": [ + "2026-02-20 20:00", + "2026-02-20 21:00", + "2026-02-20 22:00" + ], + "modelCallCount": [ + 137, + 36, + 79 + ], + "tokensUsage": [ + 4689148, + 1343019, + 3506363 + ], + "totalUsage": { + "totalModelCallCount": 1227, + "totalTokensUsage": 45867924 + } + }, + "success": true +} +``` + +### Response Fields + +| Field | Type | Description | +| ------------------------------------- | ---------------- | -------------------------------------- | +| `code` | number | HTTP status code | +| `msg` | string | Response message | +| `success` | boolean | Whether the request succeeded | +| `data.x_time` | string[] | Array of hourly timestamps | +| `data.modelCallCount` | (number\|null)[] | Model calls per hour (null if no data) | +| `data.tokensUsage` | (number\|null)[] | Tokens used per hour (null if no data) | +| `data.totalUsage.totalModelCallCount` | number | Total model calls in time range | +| `data.totalUsage.totalTokensUsage` | number | Total tokens used in time range | + +--- + +## 2. Tool Usage Query + +Query MCP tool usage (network search, web reader, zread) over a time range. + +### Endpoint + +``` +GET /api/monitor/usage/tool-usage +``` + +### Query Parameters + +| Parameter | Type | Required | Format | Description | +| ----------- | ------ | -------- | --------------------- | ------------------------- | +| `startTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | Start time of query range | +| `endTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | End time of query range | + +### Example Request + +```http +GET /api/monitor/usage/tool-usage?startTime=2026-02-20%2020%3A00%3A00&endTime=2026-02-21%2020%3A59%3A59 +Authorization: your-api-key-here +Accept-Language: en-US,en +``` + +### Example Response + +```json +{ + "code": 200, + "msg": "Operation successful", + "data": { + "x_time": [ + "2026-02-20 20:00", + "2026-02-20 21:00", + "2026-02-20 22:00" + ], + "networkSearchCount": [null, null, null], + "webReadMcpCount": [null, null, null], + "zreadMcpCount": [null, null, null], + "totalUsage": { + "totalNetworkSearchCount": 0, + "totalWebReadMcpCount": 0, + "totalZreadMcpCount": 0, + "totalSearchMcpCount": 0, + "toolDetails": [] + } + }, + "success": true +} +``` + +### Response Fields + +| Field | Type | Description | +| ----------------------------------------- | ---------------- | ----------------------------- | +| `code` | number | HTTP status code | +| `msg` | string | Response message | +| `success` | boolean | Whether the request succeeded | +| `data.x_time` | string[] | Array of hourly timestamps | +| `data.networkSearchCount` | (number\|null)[] | Network search calls per hour | +| `data.webReadMcpCount` | (number\|null)[] | Web reader MCP calls per hour | +| `data.zreadMcpCount` | (number\|null)[] | Zread MCP calls per hour | +| `data.totalUsage.totalNetworkSearchCount` | number | Total network search calls | +| `data.totalUsage.totalWebReadMcpCount` | number | Total web reader calls | +| `data.totalUsage.totalZreadMcpCount` | number | Total zread calls | +| `data.totalUsage.totalSearchMcpCount` | number | Total search MCP calls | +| `data.totalUsage.toolDetails` | array | Detailed tool usage breakdown | + +--- + +## 3. Quota Limit Query + +Query account quota limits and current usage percentages. This endpoint does not require time parameters. + +### Endpoint + +``` +GET /api/monitor/usage/quota/limit +``` + +### Query Parameters + +None required. + +### Example Request + +```http +GET /api/monitor/usage/quota/limit +Authorization: your-api-key-here +Accept-Language: en-US,en +``` + +### Example Response + +```json +{ + "code": 200, + "msg": "Operation successful", + "data": { + "limits": [ + { + "type": "TOKENS_LIMIT", + "unit": 3, + "number": 5, + "percentage": 0 + }, + { + "type": "TOKENS_LIMIT", + "unit": 6, + "number": 1, + "percentage": 21, + "nextResetTime": 1772192697998 + }, + { + "type": "TIME_LIMIT", + "unit": 5, + "number": 1, + "usage": 1000, + "currentValue": 0, + "remaining": 1000, + "percentage": 0, + "nextResetTime": 1774007097985, + "usageDetails": [ + { + "modelCode": "search-prime", + "usage": 0 + }, + { + "modelCode": "web-reader", + "usage": 0 + }, + { + "modelCode": "zread", + "usage": 0 + } + ] + } + ], + "level": "pro" + }, + "success": true +} +``` + +### Response Fields + +| Field | Type | Description | +| ------------- | ------- | ----------------------------- | +| `code` | number | HTTP status code | +| `msg` | string | Response message | +| `success` | boolean | Whether the request succeeded | +| `data.limits` | array | Array of quota limit objects | +| `data.level` | string | Account level (e.g., "pro") | + +### Limit Object Fields + +| Field | Type | Description | +| --------------- | ------ | ---------------------------------------------------------------------- | +| `type` | string | Limit type: `TOKENS_LIMIT` or `TIME_LIMIT` | +| `unit` | number | Time unit code: `3` = hour(s), `5` = month(s), `6` = week(s) | +| `number` | number | Quantity of the time unit (e.g., unit=3, number=5 means 5-hour window) | +| `percentage` | number | Usage percentage (0-100) | +| `nextResetTime` | number | Unix timestamp (ms) when quota resets | +| `usage` | number | Total quota allowed (TIME_LIMIT only) | +| `currentValue` | number | Current usage count (TIME_LIMIT only) | +| `remaining` | number | Remaining quota (TIME_LIMIT only) | +| `usageDetails` | array | Per-model usage breakdown (TIME_LIMIT only) | + +### Unit Type Mapping + +| Unit Code | Time Unit | Example | +| --------- | --------- | ------------------------------------------ | +| `3` | Hour(s) | `unit=3, number=5` → 5-hour rolling window | +| `5` | Month(s) | `unit=5, number=1` → 1-month quota | +| `6` | Week(s) | `unit=6, number=1` → 1-week quota | + +### Limit Type Explanation + +- **TOKENS_LIMIT**: Token usage quota with rolling time windows. The `unit` field defines the time unit (hour/week/month), and `number` specifies the quantity (e.g., unit=3/number=5 means a 5-hour rolling window, unit=6/number=1 means a 1-week window). +- **TIME_LIMIT**: MCP tool usage quota with fixed reset periods (e.g., monthly limit with 1000 calls). + +--- + +## Error Handling + +All endpoints return consistent error responses: + +```json +{ + "code": 400, + "msg": "Error description", + "success": false +} +``` + +### Common HTTP Status Codes + +| Code | Description | +| ---- | -------------------------------- | +| 200 | Success | +| 400 | Bad Request (invalid parameters) | +| 401 | Unauthorized (invalid API key) | +| 500 | Internal Server Error | + +--- + +## Usage Notes + +### Time Range Recommendations + +1. **Model Usage & Tool Usage**: + - Recommended range: 24-48 hours + - Data is returned in hourly buckets + - Future hours may return `null` values + +2. **Quota Limit**: + - No time parameters needed + - Returns current quota status and reset times + +### Rate Limiting + +No official rate limits documented, but recommended approach: +- Cache quota limit data (updates infrequently) +- Poll model/tool usage every 5-30 minutes +- Avoid excessive queries (< 1 request/second) + +### Date Formatting + +Always use URL-encoded format for time parameters: +``` +startTime=2026-02-20%2020%3A00%3A00 +``` + +JavaScript example: +```javascript +const startTime = '2026-02-20 20:00:00'; +const encoded = encodeURIComponent(startTime); +``` + +--- + +## Integration Example + +### Node.js with HTTPS + +```javascript +import https from 'https'; + +const API_KEY = 'your-api-key-here'; +const BASE_URL = 'https://api.z.ai'; + +function queryQuotaLimit() { + return new Promise((resolve, reject) => { + const url = new URL(`${BASE_URL}/api/monitor/usage/quota/limit`); + + const options = { + hostname: url.hostname, + port: 443, + path: url.pathname, + method: 'GET', + headers: { + 'Authorization': API_KEY, + 'Accept-Language': 'en-US,en', + 'Content-Type': 'application/json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + if (res.statusCode === 200) { + resolve(JSON.parse(data)); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + + req.on('error', reject); + req.end(); + }); +} +``` + +### TypeScript Interfaces + +```typescript +// Model Usage Response +interface ModelUsageResponse { + code: number; + msg: string; + success: boolean; + data: { + x_time: string[]; + modelCallCount: (number | null)[]; + tokensUsage: (number | null)[]; + totalUsage: { + totalModelCallCount: number; + totalTokensUsage: number; + }; + }; +} + +// Tool Usage Response +interface ToolUsageResponse { + code: number; + msg: string; + success: boolean; + data: { + x_time: string[]; + networkSearchCount: (number | null)[]; + webReadMcpCount: (number | null)[]; + zreadMcpCount: (number | null)[]; + totalUsage: { + totalNetworkSearchCount: number; + totalWebReadMcpCount: number; + totalZreadMcpCount: number; + totalSearchMcpCount: number; + toolDetails: any[]; + }; + }; +} + +// Quota Limit Response +interface QuotaLimitResponse { + code: number; + msg: string; + success: boolean; + data: { + limits: Array<{ + type: 'TOKENS_LIMIT' | 'TIME_LIMIT'; + unit: number; + number: number; + percentage: number; + nextResetTime?: number; + usage?: number; + currentValue?: number; + remaining?: number; + usageDetails?: Array<{ + modelCode: string; + usage: number; + }>; + }>; + level: string; + }; +} +``` + +--- + +## Version History + +- **2026-02-21**: Initial documentation based on `zai-org/zai-coding-plugins` v1.x +- Verified with API Key: `62e19ef7...` (redacted) +- All 3 endpoints tested and confirmed working + +--- + +## References + +- Official Plugin: https://github.com/zai-org/zai-coding-plugins +- Plugin: `glm-plan-usage` (usage query functionality) +- Source: `plugins/glm-plan-usage/skills/usage-query-skill/scripts/query-usage.mjs` diff --git a/package-lock.json b/package-lock.json index 477954e..faaca76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zai-usage-tracker", - "version": "0.0.5", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zai-usage-tracker", - "version": "0.0.5", + "version": "1.0.4", "license": "MIT", "devDependencies": { "@types/node": "^18.0.0", diff --git a/package.json b/package.json index 3330af6..41e4a00 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zai-usage-tracker", "displayName": "Z.ai GLM Usage Tracker", "description": "Track your Z.ai GLM Coding Plan usage in the status bar", - "version": "1.0.4", + "version": "1.0.5", "publisher": "melon-hub", "icon": "icon.png", "license": "MIT", @@ -56,28 +56,12 @@ "configuration": { "title": "Z.ai Usage Tracker", "properties": { - "zaiUsage.planTier": { - "type": "string", - "enum": [ - "lite", - "pro", - "max" - ], - "default": "lite", - "description": "Your GLM Coding Plan tier", - "enumDescriptions": [ - "Lite Plan: ~120 prompts every 5 hours", - "Pro Plan: ~600 prompts every 5 hours", - "Max Plan: ~2400 prompts every 5 hours" - ], - "order": 1 - }, "zaiUsage.refreshInterval": { "type": "number", "default": 5, "minimum": 1, "description": "Refresh interval in minutes", - "order": 2 + "order": 1 } } } @@ -99,4 +83,4 @@ "eslint": "^8.45.0", "typescript": "^5.1.0" } -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index e9bfa25..1c8cfd5 100644 --- a/readme.md +++ b/readme.md @@ -6,25 +6,42 @@ A VS Code extension that tracks your Z.ai GLM Coding Plan usage and displays it in the status bar. Also works with Windsurf, VSCodium, and other VS Code forks. +## Screenshots + +Plugin Screenshot + ## Features -- **Real-time Usage Display**: See your 5-hour token quota directly in the status bar - - Shows percentage used and current tokens - - Example: `✓ ⚡ 1% • 14.6K tokens` - -- **Detailed Tooltip**: Hover to see comprehensive usage stats: - - 5-hour token quota with progress bar - - 7-day usage (prompts + tokens) - - 30-day usage (prompts + tokens) - - Connection status and last update time - -- **Quick Pick Menu**: Click status bar for detailed stats and actions - - View all usage metrics - - Refresh usage data - - Configure settings - +- **Dynamic Quota Windows**: Automatically fetches all token quota windows from API + - 5-hour rolling window + - 1-week quota + - 1-month quota + - Displays the most relevant quota in status bar (prioritizes longest time window) + +- **Rich Tooltip Display**: Hover to see comprehensive usage stats with Markdown formatting: + - All token quota windows with progress bars and reset times + - MCP tool usage quotas (network search, web reader, zread) + - Today / 7-day / 30-day usage statistics + - Account plan level (auto-detected from API) + - Estimated token limits based on usage percentage + +- **Smart Status Bar**: Modern display with VS Code Codicon icons + - Example: `⚡ GLM: 21% · 45.8M / 220M Tokens` + - Warning background when usage ≥ 80% + +- **Auto-detected Plan Tier**: No manual configuration needed + - API automatically returns your account level (free/pro/enterprise) + - Displays plan tier in tooltip + +- **Precise Reset Times**: Shows exact time until quota resets + - Short periods: "in 2h 30m" + - Long periods: full date/time "2026-02-28 14:30" + +- **Debug Mode**: View raw API responses for troubleshooting + - Command: `Z.ai Usage Tracker: Debug: Show Raw API Responses` + - **Automatic Refresh**: Configurable refresh interval (default: 5 minutes) - + - **Secure API Key Storage**: Uses VS Code's encrypted SecretStorage ## Installation @@ -64,20 +81,6 @@ You need a Z.ai API key to use this extension: 4. Select "Update API Key" and paste your key 5. Your key is stored securely in VS Code's encrypted storage -### Plan Tier - -Set your GLM Coding Plan tier: - -```json -{ - "zaiUsage.planTier": "lite" // Options: "lite", "pro", "max" -} -``` - -- **Lite**: ~120 prompts every 5 hours -- **Pro**: ~600 prompts every 5 hours -- **Max**: ~2400 prompts every 5 hours - ### Refresh Interval Set how often to fetch usage data (in minutes): @@ -90,36 +93,43 @@ Set how often to fetch usage data (in minutes): Minimum: 1 minute, Default: 5 minutes +### Automatic Plan Detection + +Your GLM Coding Plan tier is automatically detected from the API - no manual configuration needed. The extension displays your account level (Lite/Pro/Max) in the tooltip. + ## Usage Once configured, the extension will: 1. Automatically activate when VS Code/Cursor starts -2. Display usage in the status bar: `✓ ⚡ 1% • 14.6K tokens` +2. Display usage in the status bar: `⚡ GLM: 21% · 45.8M / 220M Tokens` 3. Update periodically based on your refresh interval -4. Show detailed tooltip on hover -5. Provide quick actions on click +4. Show detailed tooltip with Markdown formatting on hover +5. Provide quick actions on click (refresh, configure settings) ## Commands - `zaiUsage.refresh`: Manually refresh usage data -- `zaiUsage.configure`: Open configuration menu -- `zaiUsage.showMenu`: Show quick actions menu +- `zaiUsage.configure`: Open configuration menu (API key and refresh interval) +- `zaiUsage.debug`: Debug - Show raw API responses in output channel ## Status Bar Display The status bar shows: -- **Connection Status**: ✓ (connected) or ⚠ (offline/error) -- **Icon**: Lightning bolt ⚡ -- **Percentage**: 5-hour token quota percentage (e.g., 1%) -- **Tokens**: Current tokens used (e.g., 14.6K tokens) +- **Icon**: Lightning bolt ⚡ (Codicon) +- **Label**: "GLM:" +- **Connection Status**: Empty (connected) or warning icon (error) +- **Percentage**: Usage percentage of the longest time window quota (e.g., 21%) +- **Tokens**: Actual tokens used / estimated limit (e.g., 45.8M / 220M Tokens) -Example: `✓ ⚡ 1% • 14.6K tokens` +Example: `⚡ GLM: 21% · 45.8M / 220M Tokens` Background color indicates usage level: - Normal background: < 80% quota used - Warning background: ≥ 80% quota used +The extension automatically selects the longest time window quota (Month > Week > Hour) for display in the status bar. + ## Development ```bash @@ -132,23 +142,27 @@ See [CLAUDE.md](./CLAUDE.md) for full development and publishing workflow. ## How It Works -1. **API Service**: Attempts to fetch usage data from Z.ai's API endpoints -2. **Fallback**: If no API endpoint is available, uses local tracking -3. **Configuration**: Stores API key and settings in VS Code configuration -4. **Display**: Updates status bar with current usage and progress +1. **API Service**: Fetches usage data from Z.ai's official monitor API endpoints +2. **Dynamic Quotas**: Retrieves all token quota windows (5-hour, 1-week, 1-month) and MCP tool limits +3. **Auto-detection**: Plan tier is automatically detected from API response +4. **Display**: Updates status bar with current usage, progress bars, and reset times 5. **Refresh**: Periodically fetches updated data (configurable interval) ## API Endpoints The extension uses the official Z.ai monitor API endpoints: -- `https://api.z.ai/api/monitor/usage/quota/limit` - 5-hour token quota -- `https://api.z.ai/api/monitor/usage/model-usage` - Model usage stats (with time range) +- `https://api.z.ai/api/monitor/usage/quota/limit` - Quota limits and usage percentages (returns multiple time windows) +- `https://api.z.ai/api/monitor/usage/model-usage` - Model usage stats (prompts + tokens) +- `https://api.z.ai/api/monitor/usage/tool-usage` - MCP tool usage stats + +See [API Documentation](./docs/API-DOCUMENTATION.md) for detailed API reference. ### Debugging Run the debug command to see raw API responses: - Command: `Z.ai Usage Tracker: Debug: Show Raw API Responses` +- Output appears in "Z.ai API Debug" output channel ## Privacy @@ -168,8 +182,14 @@ Run the debug command to see raw API responses: - Check your internet connection - Verify your API key is valid at [z.ai/manage-apikey](https://z.ai/manage-apikey/apikey-list) -- Try clicking "Retry" in the error message -- Run the debug command to see raw API responses +- Try clicking "Refresh Usage" +- Run the debug command: `Z.ai Usage Tracker: Debug: Show Raw API Responses` + +### Quota shows 0% or no data + +- This is normal for new accounts or after quota reset +- The 5-hour quota resets every 5 hours +- Wait a few minutes and refresh again ## License diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 0000000..f4698f0 Binary files /dev/null and b/screenshot.jpg differ diff --git a/src/api/zaiService.ts b/src/api/zaiService.ts index c51162b..c621511 100644 --- a/src/api/zaiService.ts +++ b/src/api/zaiService.ts @@ -1,9 +1,41 @@ +/** + * Represents a single token quota window from TOKENS_LIMIT + */ +export interface TokenQuota { + windowName: string; // e.g., "5-Hour", "1-Week", "1-Month" + unit: number; // 3=hour(s), 5=month(s), 6=week(s) + number: number; // Quantity of the time unit + percentage: number; // Usage percentage (0-100) + nextResetTime?: number; // Unix timestamp (ms) when quota resets + actualTokens?: number; // Actual tokens used in this window period +} + +/** + * Represents MCP tool usage limits from TIME_LIMIT + */ +export interface TimeLimit { + windowName: string; // e.g., "1-Month MCP Tools" + unit: number; // 5=month(s) + number: number; // Quantity of the time unit + percentage: number; // Usage percentage (0-100) + usage: number; // Total quota allowed + currentValue: number; // Current usage count + remaining: number; // Remaining quota + nextResetTime?: number; // Unix timestamp (ms) when quota resets + usageDetails?: Array<{ // Per-tool usage breakdown + modelCode: string; + usage: number; + }>; +} + export interface UsageData { - // 5-hour rolling window quota (token-based) - current5HourTokens: number; // Tokens used in 5-hour window - limit5HourTokens: number; // Token limit (e.g., 800M) - percentage5Hour: number; // Percentage used - quotaResetTime?: Date; + // Dynamic token quota windows from API + tokenQuotas: TokenQuota[]; // All TOKENS_LIMIT items returned by API + // MCP tool limits from API + timeLimits: TimeLimit[]; // All TIME_LIMIT items returned by API + // Today stats + todayPrompts: number; + todayTokens: number; // 7-day stats sevenDayPrompts: number; sevenDayTokens: number; @@ -13,6 +45,8 @@ export interface UsageData { // Metadata lastUpdated: Date; connectionStatus: 'connected' | 'disconnected' | 'error'; + // Plan level from quota limit API + planLevel?: string; // e.g., "free", "pro", "enterprise" } export interface FetchResult { @@ -23,13 +57,18 @@ export interface FetchResult { interface QuotaLimitResponse { limits?: Array<{ - type: string; + type: 'TOKENS_LIMIT' | 'TIME_LIMIT'; + unit: number; // 3=hour(s), 5=month(s), 6=week(s) + number: number; // Quantity of the time unit percentage: number; - currentValue?: number; + nextResetTime?: number; + // TIME_LIMIT specific fields usage?: number; - limit?: number; - usageDetails?: any; + currentValue?: number; + remaining?: number; + usageDetails?: any[]; }>; + level?: string; } interface ModelUsageResponse { @@ -42,12 +81,10 @@ interface ModelUsageResponse { export class ZaiService { private apiKey: string; - private planLimit: number; private baseUrl = 'https://api.z.ai'; - constructor(apiKey: string, planLimit: number) { + constructor(apiKey: string) { this.apiKey = apiKey; - this.planLimit = planLimit; } /** @@ -64,58 +101,122 @@ export class ZaiService { try { const now = new Date(); - const formatDateTime = (date: Date): string => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - }; - // Time windows for different stats const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); + const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0); const start7d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0); const start30d = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate(), 0, 0, 0, 0); - const queryParams7d = `?startTime=${encodeURIComponent(formatDateTime(start7d))}&endTime=${encodeURIComponent(formatDateTime(end))}`; - const queryParams30d = `?startTime=${encodeURIComponent(formatDateTime(start30d))}&endTime=${encodeURIComponent(formatDateTime(end))}`; + const queryParamsToday = `?startTime=${encodeURIComponent(this.formatDateTime(startToday))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`; + const queryParams7d = `?startTime=${encodeURIComponent(this.formatDateTime(start7d))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`; + const queryParams30d = `?startTime=${encodeURIComponent(this.formatDateTime(start30d))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`; // Fetch all endpoints in parallel - const [quotaLimitResult, modelUsage7dResult, modelUsage30dResult] = await Promise.allSettled([ + const [quotaLimitResult, modelUsageTodayResult, modelUsage7dResult, modelUsage30dResult] = await Promise.allSettled([ this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/quota/limit`, 'Quota limit'), + this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParamsToday}`, 'Today usage'), this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParams7d}`, '7-day usage'), this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParams30d}`, '30-day usage') ]); // Initialize values - let current5HourTokens = 0; - let limit5HourTokens = 800000000; // 800M default - let percentage5Hour = 0; + const tokenQuotas: TokenQuota[] = []; + const timeLimits: TimeLimit[] = []; + let todayPrompts = 0; + let todayTokens = 0; let sevenDayPrompts = 0; let sevenDayTokens = 0; let thirtyDayPrompts = 0; let thirtyDayTokens = 0; - // Process quota limit response (5-hour token quota) - if (quotaLimitResult.status === 'fulfilled' && quotaLimitResult.value) { - const quotaData = quotaLimitResult.value as QuotaLimitResponse; - if (quotaData.limits) { - for (const limit of quotaData.limits) { + // Process quota limit response - collect all TOKENS_LIMIT and TIME_LIMIT windows dynamically + const quotaLimitResponse = quotaLimitResult.status === 'fulfilled' ? quotaLimitResult.value as QuotaLimitResponse : null; + let planLevel: string | undefined; + + if (quotaLimitResponse) { + planLevel = quotaLimitResponse.level; + if (quotaLimitResponse.limits) { + for (const limit of quotaLimitResponse.limits) { if (limit.type === 'TOKENS_LIMIT') { - percentage5Hour = limit.percentage || 0; - if (limit.currentValue !== undefined) { - current5HourTokens = limit.currentValue; - } - if (limit.usage !== undefined) { - limit5HourTokens = limit.usage; - } + tokenQuotas.push({ + windowName: this.formatWindowName(limit.unit, limit.number), + unit: limit.unit, + number: limit.number, + percentage: limit.percentage || 0, + nextResetTime: limit.nextResetTime, + actualTokens: undefined // Will be filled below + }); + } else if (limit.type === 'TIME_LIMIT') { + timeLimits.push({ + windowName: this.formatWindowName(limit.unit, limit.number) + ' MCP Tools', + unit: limit.unit, + number: limit.number, + percentage: limit.percentage || 0, + usage: limit.usage || 0, + currentValue: limit.currentValue || 0, + remaining: limit.remaining || 0, + nextResetTime: limit.nextResetTime, + usageDetails: limit.usageDetails + }); } } } } + // Fetch actual usage for each token quota window based on its reset time + const quotaUsageRequests = tokenQuotas.map(async (quota) => { + if (!quota.nextResetTime || quota.percentage === 0) { + return null; + } + + try { + // Calculate the start time of this quota window + const resetDate = new Date(quota.nextResetTime); + let startDate: Date; + + // Calculate window start based on unit and number + if (quota.unit === 3) { + // Hours + startDate = new Date(resetDate.getTime() - quota.number * 3600000); + } else if (quota.unit === 6) { + // Weeks + startDate = new Date(resetDate.getTime() - quota.number * 7 * 86400000); + } else if (quota.unit === 5) { + // Months (approximate as 30 days) + startDate = new Date(resetDate.getTime() - quota.number * 30 * 86400000); + } else { + return null; + } + + const queryParams = `?startTime=${encodeURIComponent(this.formatDateTime(startDate))}&endTime=${encodeURIComponent(this.formatDateTime(now))}`; + const result = await this.fetchEndpoint( + `${this.baseUrl}/api/monitor/usage/model-usage${queryParams}`, + `${quota.windowName} usage` + ); + + if (result && (result as ModelUsageResponse).totalUsage) { + const modelData = result as ModelUsageResponse; + quota.actualTokens = modelData.totalUsage?.totalTokensUsage || 0; + } + } catch (error) { + console.error(`Failed to fetch usage for ${quota.windowName}:`, error); + } + + return null; + }); + + // Wait for all quota usage requests to complete + await Promise.allSettled(quotaUsageRequests); + + // Process today model usage + if (modelUsageTodayResult.status === 'fulfilled' && modelUsageTodayResult.value) { + const modelData = modelUsageTodayResult.value as ModelUsageResponse; + if (modelData.totalUsage) { + todayPrompts = modelData.totalUsage.totalModelCallCount || 0; + todayTokens = modelData.totalUsage.totalTokensUsage || 0; + } + } + // Process 7-day model usage if (modelUsage7dResult.status === 'fulfilled' && modelUsage7dResult.value) { const modelData = modelUsage7dResult.value as ModelUsageResponse; @@ -137,15 +238,17 @@ export class ZaiService { return { success: true, data: { - current5HourTokens, - limit5HourTokens, - percentage5Hour, + tokenQuotas, + timeLimits, + todayPrompts, + todayTokens, sevenDayPrompts, sevenDayTokens, thirtyDayPrompts, thirtyDayTokens, lastUpdated: new Date(), - connectionStatus: 'connected' + connectionStatus: 'connected', + planLevel } }; } catch (error) { @@ -158,6 +261,38 @@ export class ZaiService { } } + /** + * Format date to API datetime string format + * @param date Date to format + * @returns Formatted string like "2024-02-21 14:30:45" + */ + private formatDateTime(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } + + /** + * Format a human-readable window name from unit and number + * @param unit 3=hour(s), 5=month(s), 6=week(s) + * @param number Quantity of the time unit + * @returns Formatted string like "5-Hour", "1-Week", "1-Month" + */ + private formatWindowName(unit: number, number: number): string { + const unitNames: { [key: number]: string } = { + 3: 'Hour', + 5: 'Month', + 6: 'Week' + }; + const unitName = unitNames[unit] || 'Unknown'; + const plural = number > 1 ? 's' : ''; + return `${number}-${unitName}${plural}`; + } + /** * Fetch data from a specific endpoint * Tries both Bearer and direct token formats @@ -210,13 +345,6 @@ export class ZaiService { this.apiKey = apiKey; } - /** - * Update plan limit - */ - updatePlanLimit(planLimit: number): void { - this.planLimit = planLimit; - } - /** * Debug: Fetch and return raw API responses to see what data is available */ @@ -231,18 +359,8 @@ export class ZaiService { const start7d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0); const end7d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); - const formatDateTime = (date: Date): string => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - }; - - const queryParams24h = `?startTime=${encodeURIComponent(formatDateTime(start24h))}&endTime=${encodeURIComponent(formatDateTime(end24h))}`; - const queryParams7d = `?startTime=${encodeURIComponent(formatDateTime(start7d))}&endTime=${encodeURIComponent(formatDateTime(end7d))}`; + const queryParams24h = `?startTime=${encodeURIComponent(this.formatDateTime(start24h))}&endTime=${encodeURIComponent(this.formatDateTime(end24h))}`; + const queryParams7d = `?startTime=${encodeURIComponent(this.formatDateTime(start7d))}&endTime=${encodeURIComponent(this.formatDateTime(end7d))}`; const results = { quotaLimit: null as any, diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 1a2abac..775fd64 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -3,31 +3,19 @@ import * as vscode from 'vscode'; export interface ZaiConfiguration { /** @deprecated API key is now stored in SecretStorage. This field is only for migration. */ apiKey: string; - planTier: 'lite' | 'pro' | 'max'; refreshInterval: number; } -export const PLAN_LIMITS: Record = { - lite: 120, - pro: 600, - max: 2400 -}; - export function getConfiguration(): ZaiConfiguration { const config = vscode.workspace.getConfiguration('zaiUsage'); return { // Note: apiKey is deprecated - only read for migration from old versions // New keys are stored in VS Code SecretStorage apiKey: config.get('apiKey', ''), - planTier: config.get<'lite' | 'pro' | 'max'>('planTier', 'lite'), refreshInterval: config.get('refreshInterval', 5) }; } -export function getPlanLimit(tier: 'lite' | 'pro' | 'max'): number { - return PLAN_LIMITS[tier] || PLAN_LIMITS.lite; -} - export async function updateConfiguration(key: string, value: any): Promise { const config = vscode.workspace.getConfiguration('zaiUsage'); await config.update(key, value, vscode.ConfigurationTarget.Global); diff --git a/src/extension.ts b/src/extension.ts index fd76aa5..bfe502e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { ZaiService, UsageData } from './api/zaiService'; import { UsageIndicator } from './statusBar/usageIndicator'; -import { getConfiguration, getPlanLimit } from './config/configuration'; +import { getConfiguration } from './config/configuration'; let zaiService: ZaiService | null = null; let usageIndicator: UsageIndicator | null = null; @@ -13,7 +13,7 @@ export function activate(context: vscode.ExtensionContext) { // Create status bar indicator immediately (always visible) if (!usageIndicator) { - zaiService = new ZaiService('', getPlanLimit('lite')); + zaiService = new ZaiService(''); usageIndicator = new UsageIndicator(zaiService); usageIndicator.showNotConfigured(); } @@ -150,14 +150,11 @@ async function initializeService(context: vscode.ExtensionContext, apiKey?: stri return; } - const planLimit = getPlanLimit(config.planTier); - // Update existing service or create new one if (zaiService) { zaiService.updateApiKey(effectiveApiKey); - zaiService.updatePlanLimit(planLimit); } else { - zaiService = new ZaiService(effectiveApiKey, planLimit); + zaiService = new ZaiService(effectiveApiKey); } // Update indicator @@ -257,11 +254,6 @@ async function configureSettings(context: vscode.ExtensionContext): Promise { - if (selection === 'Configure Plan Tier') { - await promptPlanTier(context); - } else if (selection === 'Test Connection') { + if (selection === 'Test Connection') { await initializeService(context); refreshUsage(); } @@ -339,25 +328,6 @@ async function configureSettings(context: vscode.ExtensionContext): Promise { } } -/** - * Prompt user to select plan tier - */ -async function promptPlanTier(context: vscode.ExtensionContext): Promise { - const config = getConfiguration(); - - const tier = await vscode.window.showQuickPick([ - { label: 'Lite', description: '~120 prompts every 5 hours' }, - { label: 'Pro', description: '~600 prompts every 5 hours' }, - { label: 'Max', description: '~2400 prompts every 5 hours' } - ], { - placeHolder: 'Select your GLM Coding Plan tier', - canPickMany: false - }); - - if (tier) { - await vscode.workspace.getConfiguration('zaiUsage').update( - 'planTier', - tier.label.toLowerCase(), - vscode.ConfigurationTarget.Global - ); - vscode.window.showWarningMessage(`✓ Plan tier set to ${tier.label}!`); - await initializeService(context); - refreshUsage(); - } -} - export function deactivate() { console.log('Z.ai Usage Tracker extension is now deactivated'); diff --git a/src/statusBar/usageIndicator.ts b/src/statusBar/usageIndicator.ts index 67e88dc..b4b4bd6 100644 --- a/src/statusBar/usageIndicator.ts +++ b/src/statusBar/usageIndicator.ts @@ -23,16 +23,40 @@ export class UsageIndicator { this.currentError = null; // Determine connection status icon - const connectionIcon = usage.connectionStatus === 'connected' ? '✓' : '⚠'; + const connectionIcon = usage.connectionStatus === 'connected' ? '' : '$(warning)'; + + // Find the quota with the longest time window to display in status bar + // Only consider token quotas (TOKENS_LIMIT type), not tool limits (TIME_LIMIT) + // Priority: Month (5) > Week (6) > Hour (3), then compare number + const tokenQuotas = usage.tokenQuotas; + let displayQuota = tokenQuotas.length > 0 ? tokenQuotas[0] : null; + if (tokenQuotas.length > 1) { + displayQuota = tokenQuotas.reduce((max, quota) => { + const maxPriority = this.getTimeWindowPriority(max.unit, max.number); + const quotaPriority = this.getTimeWindowPriority(quota.unit, quota.number); + return quotaPriority > maxPriority ? quota : max; + }); + } + + // Format the display text + let text: string; + if (displayQuota) { + let usageInfo = ''; + if (displayQuota.actualTokens && displayQuota.percentage > 0) { + const estimatedLimit = Math.round(displayQuota.actualTokens / (displayQuota.percentage / 100)); + usageInfo = ` · ${this.formatNumber(displayQuota.actualTokens)} / ${this.formatNumber(estimatedLimit)} Tokens`; + } + text = `$(zap) GLM: ${connectionIcon} ${displayQuota.percentage}%${usageInfo}`; + } else { + text = `$(zap) GLM: ${connectionIcon} No quota data`; + } - // Format the display text: show 5-hour token quota percentage and current tokens - const tokenDisplay = this.formatNumber(usage.current5HourTokens); - const text = `${connectionIcon} $(zap) ${usage.percentage5Hour}% • ${tokenDisplay} tokens`; this.statusBarItem.text = text; this.statusBarItem.tooltip = this.getTooltip(); - // Set background color based on usage - if (usage.percentage5Hour >= 80) { + // Set background color based on highest usage + const maxPercentage = displayQuota?.percentage || 0; + if (maxPercentage >= 80) { this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); } else { this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.prominentBackground'); @@ -45,7 +69,15 @@ export class UsageIndicator { showError(error: string): void { this.currentError = error; this.statusBarItem.text = `$(error) Error`; - this.statusBarItem.tooltip = `Error fetching Z.ai usage: ${error}\nClick to refresh`; + + const md = new vscode.MarkdownString(); + md.isTrusted = true; + md.supportThemeIcons = true; // Enable Codicon icons + md.appendMarkdown('## $(error) Error Fetching Usage\n\n'); + md.appendMarkdown(`**Error**: \`${error}\`\n\n`); + md.appendMarkdown('[$(refresh) Click to Refresh](command:zaiUsage.refresh)'); + + this.statusBarItem.tooltip = md; this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); } @@ -76,11 +108,21 @@ export class UsageIndicator { /** * Get simple tooltip for status bar hover */ - private getSimpleTooltip(): string { - if (!this.currentUsage) { + private getSimpleTooltip(): vscode.MarkdownString | string { + if (!this.currentUsage || this.currentUsage.tokenQuotas.length === 0) { return 'Click to view Z.ai usage details'; } - return `Z.ai Usage: ${this.currentUsage.percentage5Hour}% of 5-hour quota\nClick to view details`; + const md = new vscode.MarkdownString(); + md.isTrusted = true; + md.supportHtml = true; + md.supportThemeIcons = true; // Enable Codicon icons + + const quotaSummary = this.currentUsage.tokenQuotas + .map(q => `**${q.windowName}**: \`${q.percentage}%\``) + .join(' • '); + md.appendMarkdown(`$(zap) **Z.ai Usage**: ${quotaSummary}\n\n`); + md.appendMarkdown('*Click to view details*'); + return md; } /** @@ -93,42 +135,132 @@ export class UsageIndicator { /** * Get tooltip with detailed information */ - private getTooltip(): string { + private getTooltip(): vscode.MarkdownString | string { if (!this.currentUsage) { return 'Click for options'; } const lastUpdated = this.formatDate(this.currentUsage.lastUpdated); + const md = new vscode.MarkdownString(); + md.isTrusted = true; + md.supportHtml = true; + md.supportThemeIcons = true; // Enable Codicon icons - let tooltip = '⚡ Z.ai GLM Usage\n'; - tooltip += '─────────────────\n\n'; - - // 5-Hour Token Quota - tooltip += `📊 5-Hour Quota (${this.currentUsage.percentage5Hour}%)\n`; - tooltip += ` ${this.formatNumber(this.currentUsage.current5HourTokens)} / ${this.formatNumber(this.currentUsage.limit5HourTokens)} tokens\n`; - tooltip += ` ${this.getProgressBar(this.currentUsage.percentage5Hour)}\n\n`; - - // 7-Day Stats - tooltip += `📅 Last 7 Days\n`; - tooltip += ` ${this.currentUsage.sevenDayPrompts} prompts • ${this.formatNumber(this.currentUsage.sevenDayTokens)} tokens\n\n`; - - // 30-Day Stats (All Time) - tooltip += `📆 Last 30 Days\n`; - tooltip += ` ${this.currentUsage.thirtyDayPrompts} prompts • ${this.formatNumber(this.currentUsage.thirtyDayTokens)} tokens\n\n`; + // Header + md.appendMarkdown('#### $(zap) Z.AI GLM Coding Plan Usage\n'); // Connection status + let statusIcon = ''; + let statusText = ''; if (this.currentUsage.connectionStatus === 'connected') { - tooltip += `✓ Connected`; + statusIcon = '$(check)'; + statusText = 'Connected'; } else if (this.currentUsage.connectionStatus === 'disconnected') { - tooltip += `⚠ Offline`; + statusIcon = '$(warning)'; + statusText = 'Offline'; } else { - tooltip += `✗ Error`; + statusIcon = '$(error)'; + statusText = 'Error'; } - tooltip += ` • Updated ${lastUpdated}\n\n`; - tooltip += 'Click for more options'; + // Build status line with plan level + let statusLine = `${statusIcon} *${statusText}*   $(clock) Updated *${lastUpdated}*`; + if (this.currentUsage.planLevel) { + const planLevelDisplay = `GLM Coding Plan **${this.currentUsage.planLevel.charAt(0).toUpperCase() + this.currentUsage.planLevel.slice(1)}**`; + statusLine += `   $(star) *${planLevelDisplay}*`; + } + md.appendMarkdown(`${statusLine}\n\n`); + + md.appendMarkdown('---\n'); + md.appendMarkdown('$(graph) *Plan Quotas*\n\n'); + md.appendMarkdown('---\n'); + + // Token Quota Windows (dynamic - display all from API) + if (this.currentUsage.tokenQuotas.length > 0) { + + // Build compact Markdown table (4 columns, no header) + md.appendMarkdown('| | | | |\n'); + md.appendMarkdown('|:--------|------:|:--------:|:----------|\n'); + + for (const quota of this.currentUsage.tokenQuotas) { + const resetInfo = quota.nextResetTime + ? `Reset ${this.formatResetTime(quota.nextResetTime)}` + : 'No reset time'; + const progressBar = this.getProgressBar(quota.percentage); + const quotaLabel = `${quota.windowName}`; + const percentageText = `${quota.percentage}%`; + + md.appendMarkdown(`| ${quotaLabel} | ${percentageText} | ${progressBar} | *${resetInfo}* |\n`); + } + + // Show estimated token limits based on usage data + const estimatedLimits = this.calculateEstimatedLimits(); + if (estimatedLimits) { + md.appendMarkdown('\n'); + md.appendMarkdown(`$(info) *Estimated limits: ${estimatedLimits}*\n\n`); + md.appendMarkdown('$(warning) *Note: Estimated based on usage % and used tokens, not official limits*\n\n'); + } + } else { + md.appendMarkdown('$(info) *No Quota Data Available*\n\n'); + } - return tooltip; + md.appendMarkdown('---\n\n'); + + // Usage Stats Table + md.appendMarkdown('$(graph) *Usage Statistics*\n\n'); + md.appendMarkdown('---\n'); + md.appendMarkdown('| | | | | |\n'); + md.appendMarkdown('|:-------|--------:|:--------:|--------:|:--------:|\n'); + md.appendMarkdown(`| $(calendar) Today  | **${this.currentUsage.todayPrompts}** | Prompts  | **${this.formatNumber(this.currentUsage.todayTokens)}** | Tokens |\n`); + md.appendMarkdown(`| $(calendar) Weeks  | **${this.currentUsage.sevenDayPrompts}** | Prompts  | **${this.formatNumber(this.currentUsage.sevenDayTokens)}** | Tokens |\n`); + md.appendMarkdown(`| $(calendar) Months  | **${this.currentUsage.thirtyDayPrompts}** | Prompts  | **${this.formatNumber(this.currentUsage.thirtyDayTokens)}** | Tokens |\n`); + md.appendMarkdown('\n'); + md.appendMarkdown('$(info) *Prompts = Model invocations (each user prompt may trigger 10-20+ calls)*\n\n'); + + // MCP Tool Limits + if (this.currentUsage.timeLimits.length > 0) { + md.appendMarkdown('---\n'); + md.appendMarkdown('$(tools) *MCP Tool Quotas*\n\n'); + md.appendMarkdown('---\n'); + md.appendMarkdown('| | | |\n'); + md.appendMarkdown('|------:|:--------:|:----------:|\n'); + + for (const timeLimit of this.currentUsage.timeLimits) { + const resetInfo = timeLimit.nextResetTime + ? `Reset ${this.formatResetTime(timeLimit.nextResetTime)}` + : 'No reset time'; + const progressBar = this.getProgressBar(timeLimit.percentage); + const usageText = `${timeLimit.currentValue}/${timeLimit.usage}`; + + md.appendMarkdown(`| ${usageText} | ${progressBar} | *${resetInfo}* |\n`); + } + + } + + md.appendMarkdown('---\n\n'); + + // Action links + md.appendMarkdown('[$(refresh) Refresh](command:zaiUsage.refresh "Fetch latest usage data")  '); + md.appendMarkdown('[$(gear) Settings](command:zaiUsage.configure "Configure API key and settings")'); + + return md; + } + + /** + * Get priority for time window comparison + * Higher priority = longer time period + * @param unit 3=hour(s), 5=month(s), 6=week(s) + * @param number Quantity of the time unit + * @returns Priority value (higher = longer period) + */ + private getTimeWindowPriority(unit: number, number: number): number { + // Priority: Month > Week > Hour + const unitPriorities: { [key: number]: number } = { + 5: 100000, // Month + 6: 10000, // Week + 3: 1000 // Hour + }; + return (unitPriorities[unit] || 0) + number; } /** @@ -145,7 +277,7 @@ export class UsageIndicator { } /** - * Format date for display + * Format date for display (for past times) */ private formatDate(date: Date): string { const now = new Date(); @@ -163,47 +295,87 @@ export class UsageIndicator { } } + /** + * Format reset time for display (for future times) + * Shows precise time with minutes for quota windows (e.g., 5-hour, 7-day limits) + */ + private formatResetTime(timestamp: number): string { + const resetDate = new Date(timestamp); + const now = new Date(); + const diffMs = resetDate.getTime() - now.getTime(); + + // Calculate time components + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + // Remaining minutes after removing full hours + const remainingMins = diffMins % 60; + // Remaining hours after removing full days + const remainingHours = diffHours % 24; + + if (diffMins < 1) { + return 'Soon'; + } else if (diffMins < 60) { + // Less than 1 hour: show minutes only + return `in ${diffMins}m`; + } else if (diffHours < 24) { + // Less than 1 day: show hours + minutes + return `in ${diffHours}h ${remainingMins}m`; + } else if (diffDays < 7) { + // Less than 1 week: show days + hours + minutes + return `in ${diffDays}d ${remainingHours}h ${remainingMins}m`; + } else { + // For longer periods, show the actual date and time in numeric format + const year = resetDate.getFullYear(); + const month = String(resetDate.getMonth() + 1).padStart(2, '0'); + const day = String(resetDate.getDate()).padStart(2, '0'); + const hours = String(resetDate.getHours()).padStart(2, '0'); + const minutes = String(resetDate.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; + } + } + /** * Get text progress bar */ private getProgressBar(percentage: number): string { const totalBars = 20; const filledBars = Math.round((percentage / 100) * totalBars); - return '█'.repeat(filledBars) + '░'.repeat(totalBars - filledBars); + return '' + '█'.repeat(filledBars) + '░'.repeat(totalBars - filledBars) + ''; } /** - * Show quick pick menu with stats and actions + * Calculate estimated token limits based on usage percentage and actual usage */ - async showQuickPick(): Promise { - const options: vscode.QuickPickItem[] = []; - - // Add stats section if we have usage data - if (this.currentUsage) { - options.push({ - label: `$(graph) 5-Hour Quota: ${this.currentUsage.percentage5Hour}%`, - description: `${this.formatNumber(this.currentUsage.current5HourTokens)} / ${this.formatNumber(this.currentUsage.limit5HourTokens)} tokens`, - kind: vscode.QuickPickItemKind.Default - }); + private calculateEstimatedLimits(): string | null { + if (!this.currentUsage || this.currentUsage.tokenQuotas.length === 0) { + return null; + } - options.push({ - label: `$(calendar) Last 7 Days`, - description: `${this.currentUsage.sevenDayPrompts} prompts • ${this.formatNumber(this.currentUsage.sevenDayTokens)} tokens` - }); + const estimates: string[] = []; - options.push({ - label: `$(history) Last 30 Days`, - description: `${this.currentUsage.thirtyDayPrompts} prompts • ${this.formatNumber(this.currentUsage.thirtyDayTokens)} tokens` - }); + for (const quota of this.currentUsage.tokenQuotas) { + // Skip if percentage is 0 to avoid division by zero, or if we don't have actual tokens + if (quota.percentage === 0 || !quota.actualTokens || quota.actualTokens === 0) { + continue; + } - // Separator - options.push({ - label: '', - kind: vscode.QuickPickItemKind.Separator - }); + // Calculate estimated total limit: actual / (percentage / 100) + const estimatedLimit = Math.round(quota.actualTokens / (quota.percentage / 100)); + estimates.push(`${quota.windowName}: ${this.formatNumber(quota.actualTokens)}/${this.formatNumber(estimatedLimit)}`); } - // Action items + return estimates.length > 0 ? estimates.join(' • ') : null; + } + + /** + * Show quick pick menu with stats and actions + */ + async showQuickPick(): Promise { + const options: vscode.QuickPickItem[] = []; + + // Action items only - detailed stats are in the tooltip options.push({ label: '$(refresh) Refresh Usage', description: 'Fetch latest usage data' @@ -211,11 +383,11 @@ export class UsageIndicator { options.push({ label: '$(settings-gear) Configure Settings', - description: 'Update API key and plan tier' + description: 'Update API key and refresh interval' }); const selected = await vscode.window.showQuickPick(options, { - placeHolder: 'Z.ai Usage Tracker' + placeHolder: 'Z.ai Usage Tracker - Select an action' }); if (selected?.label.includes('Refresh')) {