From c54fe13f793503593922d399c8830e8a5ffdc337 Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Wed, 16 Sep 2026 03:12:11 +0300 Subject: [PATCH] feat(usage): host-driven feeds for every provider, plus the extension seam The first pass had one provider and one shape. dawn wants /usage to cover what she actually pays for - antigravity, hyper, opencode-go, zai, deepseek, alibaba - and to reuse the same rules from a macOS app, so the query became a definition instead of a function. A feed is now driven by the host: `advance(state, responses)` returns either the requests to send or the finished report. The host owns HTTP, which is what lets one definition run in the CLI, in the Zig core, and in a tray. It also covers the providers that need more than one call - hyper reads /v1/teams before it can ask for that team's billing - without inventing a second concept for them. - packages/ai/docs/usage-feeds.md is the contract: report shape, credential kinds, and the unit rules (percent vs fraction vs remaining-fraction vs dollars; epoch seconds vs milliseconds vs ISO-8601) that every provider gets wrong in its own way. - seven feeds: openai-codex, anthropic (the normalised limits[] array, not the codenamed windows), google-antigravity (including the antigravity/... user-agent its endpoint gates on), hyper (plan allocation vs remaining credits, two-step, dollar balance), opencode-go, zai, and deepseek, which is balance-only and therefore has no windows at all. - reports gained `balance` and `notes`, so a provider without a percentage is not forced into one, and `skipped` became {provider, reason} - a key-only alibaba now says "needs a session credential" instead of going quiet. - credentials are provider-defined: oauth, apiKey, or a captured session. alibaba's token-plan keys are inference-only, so its quota needs a console session, and scripts/capture-alibaba-session.mjs turns a browser HAR export into the 0600 session file the bridge reads. - pi.registerProvider(..., { usage }) registers a feed from an extension into the same lookup, so a provider dawn builds herself is covered by /usage with no new wiring, and unregistering drops it. Documented in docs/custom-provider.md. Verified: 64 tests across the five usage suites (every feed driven through the host loop with scripted responses, no network in any of them), coding-agent usage 12/12 plus the CLI and interactive suites, tsgo and biome clean. --- tests/fixtures/alibaba-reset-card_list.json | 25 +++ tests/fixtures/alibaba-subscription.json | 33 ++++ tests/fixtures/alibaba-usage.json | 28 +++ tests/fixtures/anthropic-profile.json | 33 ++++ tests/fixtures/anthropic-usage.json | 94 ++++++++++ tests/fixtures/antigravity-quota.json | 198 ++++++++++++++++++++ tests/fixtures/deepseek-balance.json | 11 ++ tests/fixtures/hyper-billing.json | 35 ++++ tests/fixtures/hyper-members.json | 10 + tests/fixtures/hyper-teams.json | 10 + tests/fixtures/opencode-go-usage.json | 19 ++ tests/fixtures/zai-usage.json | 40 ++++ 12 files changed, 536 insertions(+) create mode 100644 tests/fixtures/alibaba-reset-card_list.json create mode 100644 tests/fixtures/alibaba-subscription.json create mode 100644 tests/fixtures/alibaba-usage.json create mode 100644 tests/fixtures/anthropic-profile.json create mode 100644 tests/fixtures/anthropic-usage.json create mode 100644 tests/fixtures/antigravity-quota.json create mode 100644 tests/fixtures/deepseek-balance.json create mode 100644 tests/fixtures/hyper-billing.json create mode 100644 tests/fixtures/hyper-members.json create mode 100644 tests/fixtures/hyper-teams.json create mode 100644 tests/fixtures/opencode-go-usage.json create mode 100644 tests/fixtures/zai-usage.json diff --git a/tests/fixtures/alibaba-reset-card_list.json b/tests/fixtures/alibaba-reset-card_list.json new file mode 100644 index 0000000..4cced7e --- /dev/null +++ b/tests/fixtures/alibaba-reset-card_list.json @@ -0,0 +1,25 @@ +{ + "code": "200", + "data": { + "DataV2": { + "ret": [ + "SUCCESS::接口调用成功" + ], + "data": { + "msg": "Success.", + "code": "SUCCESS", + "data": [], + "requestId": "00000000-0000-0000-0000-000000000005", + "success": true + } + }, + "success": true, + "httpStatus": 200, + "errorCode": "", + "api": "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/reset-card/list", + "errorMsg": "" + }, + "httpStatusCode": "200", + "requestId": "00000000-0000-0000-0000-000000000006", + "successResponse": true +} diff --git a/tests/fixtures/alibaba-subscription.json b/tests/fixtures/alibaba-subscription.json new file mode 100644 index 0000000..5f76640 --- /dev/null +++ b/tests/fixtures/alibaba-subscription.json @@ -0,0 +1,33 @@ +{ + "code": "200", + "data": { + "DataV2": { + "ret": [ + "SUCCESS::接口调用成功" + ], + "data": { + "msg": "Success.", + "code": "SUCCESS", + "data": { + "instanceCode": "sfm_tokenplansolo_public_intl-sg-sanitized001", + "specCode": "pro", + "remainingDays": 29, + "startTime": 1789415534000, + "endTime": 1792080000000, + "autoRenewFlag": false, + "status": "VALID" + }, + "requestId": "00000000-0000-0000-0000-000000000003", + "success": true + } + }, + "success": true, + "httpStatus": 200, + "errorCode": "", + "api": "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription", + "errorMsg": "" + }, + "httpStatusCode": "200", + "requestId": "00000000-0000-0000-0000-000000000004", + "successResponse": true +} diff --git a/tests/fixtures/alibaba-usage.json b/tests/fixtures/alibaba-usage.json new file mode 100644 index 0000000..172d02a --- /dev/null +++ b/tests/fixtures/alibaba-usage.json @@ -0,0 +1,28 @@ +{ + "code": "200", + "data": { + "DataV2": { + "ret": [ + "SUCCESS::接口调用成功" + ], + "data": { + "msg": "Success.", + "code": "SUCCESS", + "data": { + "per1WeekResetTime": 1790021160000, + "per1WeekPercentage": 0.83603712139856 + }, + "requestId": "00000000-0000-0000-0000-000000000001", + "success": true + } + }, + "success": true, + "httpStatus": 200, + "errorCode": "", + "api": "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage", + "errorMsg": "" + }, + "httpStatusCode": "200", + "requestId": "00000000-0000-0000-0000-000000000002", + "successResponse": true +} diff --git a/tests/fixtures/anthropic-profile.json b/tests/fixtures/anthropic-profile.json new file mode 100644 index 0000000..282777a --- /dev/null +++ b/tests/fixtures/anthropic-profile.json @@ -0,0 +1,33 @@ +{ + "account": { + "uuid": "00000000-0000-4000-8000-000000000000", + "full_name": "Example User", + "display_name": "example", + "email": "user@example.com", + "has_claude_max": false, + "has_claude_pro": false, + "created_at": "2026-06-09T11:28:39.481128Z" + }, + "organization": { + "uuid": "11111111-1111-4111-8111-111111111111", + "name": "Example Org", + "organization_type": "claude_team", + "billing_type": "stripe_subscription", + "rate_limit_tier": "default_raven", + "seat_tier": "team_standard", + "has_extra_usage_enabled": false, + "subscription_status": "active", + "subscription_created_at": "2026-06-29T10:33:57.742144Z", + "cc_onboarding_flags": {}, + "claude_code_trial_ends_at": null, + "claude_code_trial_duration_days": null, + "payment_auth_hosted_invoice_url": null, + "claude_ai_completion_feedback_enabled": true + }, + "application": { + "uuid": "22222222-2222-4222-8222-222222222222", + "name": "Claude Code", + "slug": "claude-code" + }, + "enabled_plugins": [] +} diff --git a/tests/fixtures/anthropic-usage.json b/tests/fixtures/anthropic-usage.json new file mode 100644 index 0000000..096fd11 --- /dev/null +++ b/tests/fixtures/anthropic-usage.json @@ -0,0 +1,94 @@ +{ + "five_hour": { + "utilization": 0.0, + "resets_at": null, + "limit_dollars": null, + "used_dollars": null, + "remaining_dollars": null, + "locked_reason": null + }, + "seven_day": { + "utilization": 94.0, + "resets_at": "2026-09-16T08:00:00.037593+00:00", + "limit_dollars": null, + "used_dollars": null, + "remaining_dollars": null, + "locked_reason": null + }, + "seven_day_oauth_apps": null, + "seven_day_opus": null, + "seven_day_sonnet": null, + "seven_day_cowork": null, + "seven_day_omelette": null, + "tangelo": null, + "iguana_necktie": null, + "omelette_promotional": null, + "nimbus_quill": { + "utilization": 0.0, + "resets_at": null, + "limit_dollars": null, + "used_dollars": null, + "remaining_dollars": null, + "locked_reason": null + }, + "cinder_cove": null, + "copper_kite": null, + "harbor_lantern": null, + "amber_ladder": null, + "juniper_tide": null, + "cedar_ember": null, + "extra_usage": { + "is_enabled": false, + "monthly_limit": null, + "used_credits": null, + "utilization": null, + "currency": null, + "decimal_places": null, + "disabled_reason": null, + "user_disabled": true, + "spend_limit_reached": false, + "credits_ever_enabled": true, + "daily": null, + "weekly": null + }, + "limits": [ + { + "kind": "session", + "group": "session", + "percent": 0, + "severity": "normal", + "resets_at": null, + "scope": null, + "is_active": false + }, + { + "kind": "weekly_all", + "group": "weekly", + "percent": 94, + "severity": "critical", + "resets_at": "2026-09-16T08:00:00.037593+00:00", + "scope": null, + "is_active": true + } + ], + "spend": { + "used": { + "amount_minor": 0, + "currency": "USD", + "exponent": 2 + }, + "limit": null, + "percent": 0, + "severity": "normal", + "enabled": false, + "disabled_reason": null, + "cap": null, + "balance": null, + "auto_reload": null, + "disclaimer": "Usage credits cover you when you hit your plan limits. [Learn more](https://support.claude.com/articles/12429409)", + "can_purchase_credits": false, + "can_toggle": false + }, + "member_dashboard_available": false, + "seven_day_breakdown": null +} diff --git a/tests/fixtures/antigravity-quota.json b/tests/fixtures/antigravity-quota.json new file mode 100644 index 0000000..69a5d18 --- /dev/null +++ b/tests/fixtures/antigravity-quota.json @@ -0,0 +1,198 @@ +{ + "buckets": [ + { + "tokenType": "WTUS", + "modelId": "chat_20706", + "remainingFraction": 1 + }, + { + "tokenType": "WTUS", + "modelId": "chat_23310", + "remainingFraction": 1 + }, + { + "resetTime": "2026-09-16T04:40:57Z", + "tokenType": "WTUS", + "modelId": "claude-opus-4-6-thinking", + "remainingFraction": 1 + }, + { + "resetTime": "2026-09-16T04:40:57Z", + "tokenType": "WTUS", + "modelId": "claude-sonnet-4-6", + "remainingFraction": 1 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-2.5-flash", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-2.5-flash-lite", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-2.5-flash-thinking", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-2.5-pro", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3-flash", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3-flash-agent", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.1-flash-image", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.1-flash-lite", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.1-pro-high", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.1-pro-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.5-flash-extra-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.5-flash-lite", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.5-flash-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.6-flash-high", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.6-flash-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.6-flash-medium", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.6-flash-tiered", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.7-flash-high", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.7-flash-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.7-flash-medium", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.7-flash-tiered", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.8-flash-high", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.8-flash-low", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.8-flash-medium", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-3.8-flash-tiered", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-15T23:46:00Z", + "tokenType": "WTUS", + "modelId": "gemini-pro-agent", + "remainingFraction": 0.16592255 + }, + { + "resetTime": "2026-09-16T04:40:57Z", + "tokenType": "WTUS", + "modelId": "gpt-oss-120b-medium", + "remainingFraction": 1 + }, + { + "tokenType": "WTUS", + "modelId": "tab_flash_lite_preview", + "remainingFraction": 1 + }, + { + "tokenType": "WTUS", + "modelId": "tab_jump_flash_lite_preview", + "remainingFraction": 1 + } + ] +} diff --git a/tests/fixtures/deepseek-balance.json b/tests/fixtures/deepseek-balance.json new file mode 100644 index 0000000..af2a5e2 --- /dev/null +++ b/tests/fixtures/deepseek-balance.json @@ -0,0 +1,11 @@ +{ + "is_available": true, + "balance_infos": [ + { + "currency": "USD", + "total_balance": "0.03", + "granted_balance": "0.00", + "topped_up_balance": "0.03" + } + ] +} diff --git a/tests/fixtures/hyper-billing.json b/tests/fixtures/hyper-billing.json new file mode 100644 index 0000000..3147f4b --- /dev/null +++ b/tests/fixtures/hyper-billing.json @@ -0,0 +1,35 @@ +{ + "plan": { + "id": "redacted-plan-id", + "name": "Shred", + "description": "Great for solo developers working with AI every day", + "type": "paid", + "price_cents": 2000, + "period": "month", + "hypercredits": 250 + }, + "status": "active", + "balances": { + "plan": { + "usd": "11.83", + "hypercredits": "237" + }, + "purchased": { + "usd": "0.00", + "hypercredits": "0" + }, + "total": { + "usd": "11.83", + "hypercredits": "237" + } + }, + "billing_period": { + "start": "2026-09-15T22:26:04Z", + "end": "2026-10-15T22:26:04Z" + }, + "hypercredit_refresh": { + "last_at": "2026-09-15T22:26:09.702198Z", + "next_at": "2026-09-16T22:26:09.702198Z" + }, + "credit_refresh_scope": "team" +} diff --git a/tests/fixtures/hyper-members.json b/tests/fixtures/hyper-members.json new file mode 100644 index 0000000..374a576 --- /dev/null +++ b/tests/fixtures/hyper-members.json @@ -0,0 +1,10 @@ +{ + "items": [ + { + "id": "usr_789456123", + "email": "user@example.com", + "name": "Example User", + "role": "admin" + } + ] +} diff --git a/tests/fixtures/hyper-teams.json b/tests/fixtures/hyper-teams.json new file mode 100644 index 0000000..88b995c --- /dev/null +++ b/tests/fixtures/hyper-teams.json @@ -0,0 +1,10 @@ +{ + "items": [ + { + "id": "team_20439d8c54af", + "name": "Example Team", + "created_at": "2026-09-15T22:25:23.652189Z", + "updated_at": "2026-09-15T22:25:23.652189Z" + } + ] +} diff --git a/tests/fixtures/opencode-go-usage.json b/tests/fixtures/opencode-go-usage.json new file mode 100644 index 0000000..32957ea --- /dev/null +++ b/tests/fixtures/opencode-go-usage.json @@ -0,0 +1,19 @@ +{ + "usage": { + "rolling": { + "status": "ok", + "percent": 0, + "resetsAt": "2026-09-16T04:40:14.037Z" + }, + "weekly": { + "status": "ok", + "percent": 0, + "resetsAt": "2026-09-21T00:00:00.037Z" + }, + "monthly": { + "status": "rate-limited", + "percent": 100, + "resetsAt": "2026-09-29T07:01:56.037Z" + } + } +} diff --git a/tests/fixtures/zai-usage.json b/tests/fixtures/zai-usage.json new file mode 100644 index 0000000..72a351f --- /dev/null +++ b/tests/fixtures/zai-usage.json @@ -0,0 +1,40 @@ +{ + "code": 200, + "msg": "Operation successful", + "data": { + "limits": [ + { + "type": "TIME_LIMIT", + "unit": 5, + "number": 1, + "usage": 100, + "currentValue": 0, + "remaining": 100, + "percentage": 0, + "nextResetTime": 1792027143998, + "usageDetails": [ + { + "modelCode": "search-prime", + "usage": 0 + }, + { + "modelCode": "web-reader", + "usage": 0 + }, + { + "modelCode": "zread", + "usage": 0 + } + ] + }, + { + "type": "TOKENS_LIMIT", + "unit": 3, + "number": 5, + "percentage": 0 + } + ], + "level": "lite" + }, + "success": true +} -- 2.54.0