mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
* fix(gemini): preserve structured tool calls for antigravity * fix(gemini): parse prefixed textual tool calls * fix(antigravity): preserve textual SSE tool calls * fix(stream): normalize textual passthrough tool calls * fix(stream): normalize split textual tool calls * fix(stream): suppress malformed textual tool calls * fix(stream): suppress compact malformed tool calls * fix(stream): emit structured textual tool calls * fix(stream): suppress unknown textual tool calls * fix(stream): normalize responses textual tool calls * chore: ignore .claude/settings.local.json (per-user Claude Code permissions) * fix(opencode-go): route qwen3.x via claude messages + repair fixMissingToolResponses for Claude-shape upstreams (#2791) Integrated into release/v3.8.6 * fix: resolve npm install warnings — remove dead deps, relax engine constraint (#2792) Integrated into release/v3.8.6 * fix: register missing web-cookie validators (claude-web, gemini-web, copilot-web, t3-web) (#2793) Integrated into release/v3.8.6 * fix: Error: Unable to inspect existing database #2771 (#2795) Integrated into release/v3.8.6 * fix(oauth): repair Google loopback callback flow (#2796) Integrated into release/v3.8.6 * feat(logs): add clean history button (#2799) Integrated into release/v3.8.6 * [codex] home: restore settings-driven home layout and quota auto-refresh (#2800) Integrated into release/v3.8.6 * fix(gemini): emit signaturelessToolCallMode:text for GEMINI format models (#2801) Integrated into release/v3.8.6 * feat(modelSpecs): align opencode-go family with upstream provider limits (#2802) Integrated into release/v3.8.6 * chore: apply unit test fixes, polyfills, and environment precedence fixes * docs(agents): atualiza fluxos de release e triagem Expande os workflows de release para incluir auditoria de segurança, CHANGELOG completo por commits, quality gate obrigatório, homologação em VPS local, publicação oficial, deploy em Akamai e validação de artefatos. Reorganiza a triagem de features com arquivos permanentes por bucket, suporte a itens em andamento, regra de reclaim após 15 dias e novo tratamento para ideias viáveis catalogadas. Corrige a orientação de revisão de discussões para usar a ordem cronológica real dos comentários e respostas ao identificar a última atividade. * fix(lockout): classify Gemini Antigravity resource exhaustion as quota_exhausted * fix(reasoning): gate replay by interleaved field * docs(rule-16): permit human Co-authored-by, restrict only AI/bot trailers Rule #16 previously banned all `Co-Authored-By` trailers absolutely. That blocked the upstream-port workflows (`/port-upstream-features` and `/port-upstream-issues`), which must credit human upstream PR authors and issue reporters in OmniRoute commits. Refine the rule to ban only AI/bot-attributed trailers (Claude, GPT, Copilot, Bot; anthropic.com / openai.com / bot-owned noreply.github.com emails) while allowing standard human `Co-authored-by: Name <email>` attribution. Sync the rule across the source CLAUDE.md, the E2E shakedown doc note, and 41 i18n translations. * fix(gitlawb): add specialty validators for connection test — bypass /models probe GitLawB OpenGateway API (xiaomi-mimo compatible) does not expose a /models endpoint, causing validateOpenAILikeProvider to 404 on the initial probe and report 'Provider validation endpoint not supported'. Add specialty validators for both gitlawb and gitlawb-gmi that follow the same pattern as the existing xiaomi-mimo validator: skip GET /models, validate directly via POST /chat/completions with a minimal test message. Any 401/403 response means an invalid key; all other responses mean auth is OK. Fixes test-connection returning 404 for GitLawB providers. * test(gitlawb): add 12 unit tests for gitlawb and gitlawb-gmi specialty validators Covers success, auth failure (401/403), non-auth acceptance (400/422/429), network errors, and custom baseUrl overrides for both providers. * feat(gitlawb): serve models from static registry without API-unavailable warning GitLawB's OpenGateway API does not expose a /models endpoint per provider-path. Previously the models route fell through to the generic fallback which returned static catalog models with the misleading 'API unavailable — using local catalog' warning. Now gitlawb and gitlawb-gmi are handled as static model providers (same pattern as reka and qwen OAuth) — models are served from the provider registry without any warning, since all registered models are functional via POST /chat/completions. * refactor(gitlawb): extract shared opengateway validator factory, fix docs path in test - Extract gitlawb/gitlawb-gmi validators into buildOpengatewayValidator factory - Fix dockerignore-docs-coverage test: update stale docs/AUTO-COMBO.md -> docs/routing/AUTO-COMBO.md * fix(reasoning): guard interleaved capability lookup * feat(gitlawb): dynamic model fetch with gmi-cloud fallback Hybrid approach: - gitlawb (xiaomi-mimo): dynamic /models endpoint → 356 models - gitlawb-gmi (gmi-cloud): 404 fallback → local catalog gracefully Mimics Gitlawb/openclaude's model-routing pattern * i18n(pt-BR): complete missing translations and sync with en.json * feat(build): nix multi-OS package manager install (#2806) Integrated into release/v3.8.6 * fix(i18n): translate 144 new __MISSING__ pt-BR strings (#2816) Integrated into release/v3.8.6 * chore(docs): set coverage gate to 40/40/40/40 in CLAUDE.md Aligns the documented coverage gate with the v3.8.6 release decision (lowered from 75/75/75/70). Matches the threshold already set in package.json by the large feature PRs (planos 11-22). * fix(cli): respect PORT env var in serve command (#2845) Integrated into release/v3.8.6. * fix(deepseek-web): return 400 when client sends tools[] - chat.deepseek.com has no tool support (#2854) Integrated into release/v3.8.6. * fix(qoder): reject invalid/expired PATs returning Cosy 500 error (#2860) Integrated into release/v3.8.6. * fix(cli): register openclaw in tool-detector (#2833) (#2850) Integrated into release/v3.8.6. * fix(api): include noAuth providers in /v1/models catalog (#2798) (#2814) Integrated into release/v3.8.6. * fix(combo): resolve custom provider targets via combo name (#2778) (#2812) Integrated into release/v3.8.6. * fix(translator): strip safety_identifier in openai-responses cleanup (#2770) (#2809) Integrated into release/v3.8.6. * fix(quota): honor explicit per-connection preflight opt-out (#2831) (#2844) Integrated into release/v3.8.6. * fix(usage): un-invert GitHub Copilot Free/limited quota — limited_user_quotas is remaining (#2876) (#2881) Integrated into release/v3.8.6. * fix(nous-research): correct baseUrl to include /chat/completions (#2826) (#2835) Integrated into release/v3.8.6. * fix(opencode): qwen3.x max/plus models lack vision support (#2822) (#2836) Integrated into release/v3.8.6. * fix(translator): pass-through tool_search built-in tool type (#2766) (#2811) Integrated into release/v3.8.6. * fix(github): route claude-opus-4.6 via chat completions (#2821) Integrated into release/v3.8.6. * docs(oauth): add Windsurf login fix design (Phase 1 hotfix + Phase 2 Firebase OAuth) Two-phase plan to fix the broken Windsurf OAuth flow: - Phase 1: drop the dead app.devin.ai/editor/signin PKCE path, promote import-token from windsurf.com/show-auth-token as the primary path - Phase 2: port Firebase OAuth + RegisterUser flow from fendoushaonian/WindSurf-gRPC-API for full browser-based automation Spec only - no code changes yet. * docs(plan): Phase 1 windsurf login hotfix implementation plan 10 tasks covering: - TDD assertions for flowType + 410 Gone responses - Provider switch to import_token - Route handler retiring authorize/start-callback-server/poll-callback - OAuthModal UI override - i18n sync - Verification + PR steps * fix(cli): replace cli-table3 with hand-rolled formatter (#2752) (#2813) Integrated into release/v3.8.6. * fix(skills): skip interception for unregistered client-native tools (#2815) (#2817) Integrated into release/v3.8.6. * feat(sse): add RTK filters for kubectl, docker-build, composer, gh (#2824) Integrated into release/v3.8.6. * fix(geminiHelper): support rec.image content shape + warn on dropped remote URLs (refs #2807) (#2855) Integrated into release/v3.8.6. * fix(cli): allow nullable/optional apiKey in cliMitmStartSchema (#2857) Integrated into release/v3.8.6. * fix(combo): preserve system messages during context handoff summary generation (#2865) Integrated into release/v3.8.6. * fix: wire CLIProxyAPI fallback settings into chatCore routing engine (#2866) Integrated into release/v3.8.6. * fix(usage): add opencode quota fetcher (#2852) (#2867) Integrated into release/v3.8.6. * feat(claude): default xhigh support for newer Opus models (#2874) Integrated into release/v3.8.6. * fix(cli): restore omniroute logs command stream (#2756) (#2810) Integrated into release/v3.8.6. * fix(combo): normalize upstream Headers for Node 24 undici interop (#2751) (#2823) Integrated into release/v3.8.6. * Rename proxy log Public IP to Client IP (#2880) Integrated into release/v3.8.6. * fix(claude): preserve max effort for supported models (#2875) Integrated into release/v3.8.6. * fix(oauth): switch windsurf provider to import_token flow The PKCE auth URL targeting app.devin.ai/editor/signin returns 404 post-rebrand. Until Phase 2 ports Firebase OAuth + RegisterUser, the only supported path is import-token via windsurf.com/show-auth-token. - windsurf.ts: drop buildAuthUrl, set flowType=import_token - generateAuthData returns supported:false + helpful error for windsurf/devin-cli - tests: assert flowType + disabled stub * fix(oauth): return 410 Gone for retired windsurf/devin-cli PKCE actions start-callback-server, authorize, and poll-callback (GET + POST) now return 410 Gone with a pointer to /import-token. The 410 short-circuit runs before auth so the response is honest about the action being permanently gone, not gated. Codex PKCE flow unchanged. Tests: 5 new assertions cover GET + POST 410 paths and a Codex regression check. * refactor(oauth): annotate retired PKCE fields in WINDSURF_CONFIG No behaviour change - comment-only update documenting that authorizeUrl, codeChallengeMethod, callbackPort, callbackPath, apiServerUrl, and exchangePath are no longer consumed. Active fields (inferenceUrl, showAuthTokenUrl, firebaseApiKey, ideName) called out separately. * fix(cli,docs): use requireCliToolsAuth in logs route + document OPENCODE quota env Post-merge contract fixes for v3.8.6: - src/app/api/cli-tools/logs/route.ts (#2810) now uses the shared requireCliToolsAuth guard (param renamed req->request) to satisfy the cli-tools-auth-hardening contract test. - Document OMNIROUTE_OPENCODE_QUOTA_URL (#2867) in docs/reference/ENVIRONMENT.md to satisfy the env/docs sync contract. * fix(dashboard): force import-token panel for windsurf/devin-cli Phase 1 hotfix: hide the 'Browser Login' tab and start in Paste API Key mode. Removes windsurf/devin-cli from PKCE_CALLBACK_SERVER_PROVIDERS so no callback server is started for them. Codex still uses the PKCE flow. The 'Get token' link continues to point at windsurf.com/show-auth-token via the existing supportsTokenPaste form copy. * fix(oauth): windsurf import-token mapTokens signature mismatch The route at `src/app/api/oauth/[provider]/[action]/route.ts` invokes `providerData.mapTokens({ accessToken: token })` (object), matching the cursor/kiro signature. The windsurf provider was declared with `mapTokens(token: string)` instead, so the entire object was stored as `accessToken`. When the connection record reached the SQLite layer it crashed with: SQLite3 can only bind numbers, strings, bigints, buffers, and null Fix by aligning windsurf's `mapTokens` signature with the route caller and the cursor/kiro convention. Also dedupe a copy-pasted second `if (action === "import-token")` block in the route handler — the second block was unreachable but identical to the first. Adds two regression tests asserting that `provider.mapTokens({ accessToken })` returns a string `accessToken` for both windsurf and devin-cli, so a future signature drift trips the gate instead of the SQLite bind error in production. * feat(compression): expand pt-BR pack with troglodita rules (15 → 49) (#2818) Integrated into release/v3.8.6 * fix(sse): repair RTK engine defaults so dedup and direct calls work (#2825) Integrated into release/v3.8.6 * fix(mcp): redirect console.log/warn to stderr in --mcp stdio mode (#2840) Integrated into release/v3.8.6 * fix(gemini-cli): prefer real project IDs over default-project (#2841) Integrated into release/v3.8.6 * fix(opencode-go): add provider limits quota fetcher (#2861) Integrated into release/v3.8.6 * Audit & add web cookie providers: fix 4 missing registry entries + DuckDuckGo (#2862) Integrated into release/v3.8.6 * fix(antigravity): harden signatureless tool history (#2878) Integrated into release/v3.8.6 * fix: provider model sync pruning and dynamic antigravity MITM proxy mappings (#2886) Integrated into release/v3.8.6 * feat(usage): per-API-key token limits scoped to model/provider/global (#2888) Integrated into release/v3.8.6 * fix(audio): build multipart body manually to preserve Content-Type (#2842) Integrated into release/v3.8.6 * refactor: remove agent skill documentation files and streamline maintenance workflows * test(stabilization): resolve unit test failures in blackbox-web, schema-coercion, translator-helper-branches, usage-service-hardening, and audio-transcription * fix(security): mitigate Socket.dev supply-chain findings + secrets opt-in + minimal build profile (#2863) (#2871) Two real security gaps closed and four cosmetic Socket.dev fingerprints removed. See docs/security/SOCKET_DEV_FINDINGS.md for the per-finding maintainer attestation. Real bugs fixed: - cloudSync: HMAC verification of `X-Cloud-Sig` + opt-in `OMNIROUTE_CLOUD_SYNC_SECRETS=true` before overwriting `accessToken` / `refreshToken` / `providerSpecificData` from a remote response. Closes the silent-credential-swap surface (a misconfigured or hostile CLOUD_URL could previously replace local tokens unverified). - Zed import: split into 2-step `/discover` + `/import` flow. `/import` now requires `confirmedAccounts: [{ service, account, fingerprint }]` and re-reads the keychain server-side to filter by fingerprint, so a tampered discover response cannot trick the endpoint into saving an unrelated token. Cosmetic Socket.dev mitigations: - runElevatedPowerShell writes the elevated payload to a per-call temp `.ps1` file (mode 0o600) and references it via `-File`. Removes the textbook `-EncodedCommand <base64utf16le>` pattern flagged as malware by Socket's AI classifier. - Maintainer attestation `SECURITY-AUDITOR-NOTE:` blocks added at every flagged call site pointing to `docs/security/SOCKET_DEV_FINDINGS.md`. Build-time hardening: - `OMNIROUTE_BUILD_PROFILE=minimal` (`npm run build:secure`) physically removes the four sensitive modules from the standalone bundle via webpack `NormalModuleReplacementPlugin`. Stubs throw `FeatureDisabledError` at runtime. Intended for the `omniroute-secure` artifact. Tests: - 24 new unit tests in `tests/unit/security/` covering the wrapper builder, HMAC verification (4 cases), credential fingerprint determinism (5 cases), confirmedAccounts validation + fingerprint filtering (6 cases), and the minimal-build stubs (5 cases). Docs: - New `docs/security/SOCKET_DEV_FINDINGS.md` — per-finding attestation. - New `socket.yml` — Socket.dev v2 config pointing at the attestation. - Updated `SECURITY.md` — supply-chain scanner section. - Updated `.env.example` — three new env vars documented. Backwards compatibility: - Cloud sync token overwrite is OFF by default. Users who relied on it must set `OMNIROUTE_CLOUD_SYNC_SECRETS=true`. Breaking change documented in CHANGELOG. - Zed import 2-step is the new default; legacy 1-step preserved behind `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP=true` and will be removed in v3.9. Closes #2863 * fix(security): redact public Firebase Web key from windsurf spec; doc SHA-256 cache-key rationale (#2894) Two security-scanning findings on release/v3.8.6: - Secret-scanning alert 7 (google_api_key): the windsurf login-fix design spec embedded the literal public Firebase Web API key on two lines. Firebase Web API keys are non-sensitive by design (they identify the project; access is gated by Firebase Security Rules + key restrictions), but the literal trips secret scanning. Redacted to a placeholder; the embedded default still goes through resolvePublicCred per rule #11. - Code-scanning alert 261 (js/insufficient-password-hash): tokenCacheKey() uses SHA-256 to derive an in-memory cache key from the session token, not for password-at-rest storage. Added a comment documenting why CWE-916 KDFs do not apply (false positive). * fix(ci): resolve release/v3.8.6 gate failures (docs-sync, any-budget, pack-artifact) (#2895) * fix(ci): resolve release/v3.8.6 gate failures (docs-sync, any-budget, pack-artifact) Three CI gates failed on release/v3.8.6 (run 26630300877): - docs-sync: CHANGELOG had a spurious "## [3.8.6-patch]" section above "## [3.8.6]", so the latest release no longer matched package.json (3.8.6) and the 41 i18n CHANGELOG mirrors were flagged as missing that section. Fold the lone #2752 entry into [3.8.6] and drop the patch heading. - any-budget:t11: open-sse/handlers/chatCore.ts regressed to 1 explicit `any` (budget 0). Type the persist callback arg as Record<string, unknown>, which matches runWithOnPersist's RefreshPersistFn contract exactly. - pack-artifact: open-sse/utils/setupPolyfill.ts ships via package.json "files" (bin/omniroute.mjs imports it at startup) but was missing from the pack policy allowlist. Allow it and add a regression test. * fix(security): redact public Firebase Web key from windsurf spec Redact the literal public Firebase Web API key (secret-scanning #7) to a placeholder, mirroring the redaction on release/v3.8.6 (PR #2894) and the windsurf fix branch. Non-sensitive public Web key; trips secret scanning. * feat(combo): Zero-Latency Combos (Hedging, Proactive Compression, Predictive TTFT) (#2868) * feat(combo): implement zero-latency combo optimizations (hedging, proactive compression, predictive TTFT) * fix(combo): fix predictive TTFT skip logic and unhandled promise rejections --------- Co-authored-by: Automation <automation@omniroute> * feat: implement automated skill workflows and update system configuration and validation schemas * test: eliminate dynamic cast warnings in cloud-sync unit test * test: isolate services-branch-hardening database directory to avoid concurrency issues * feat(providers): add 7 new web-cookie providers + research catalog + discovery tool New providers: - huggingchat: free LLM chat via huggingface.co/chat (no subscription) - phind: free dev-focused AI chat via phind.com/api/agent - poe-web: multi-model chat via poe.com GraphQL (p-b cookie) - venice-web: privacy-focused AI chat via venice.ai (session cookie) - v0-vercel-web: Vercel v0 code gen via v0.dev (session cookie) - kimi-web: Moonshot Kimi chat via kimi.moonshot.cn (session cookie) - doubao-web: ByteDance Doubao chat via doubao.com (session cookie) Additional: - Research catalog: docs/research/UNLIMITED_LLM_ACCESS.md - Discovery tool design + stub: src/lib/discovery/ + migration 073 - Unit tests: 33 tests for all 7 providers - Shared helpers consolidated in error.ts (slop cleanup) - All registered in WEB_COOKIE_PROVIDERS + providerRegistry + webSessionCredentials Closes #2885 * fix(typecheck): resolve typecheck errors in combo spec and compression modules * feat(api,oauth): add `agy` (Antigravity CLI) standalone provider with CLI token import (#2899) Add a standalone OAuth provider `agy` (Antigravity CLI) next to gemini-cli/antigravity. It reuses the antigravity inference backend (identical Google client_id + daily-cloudcode-pa.googleapis.com endpoint, executor and token-refresh) but ships its own model catalog — including the Claude models the backend exposes (claude-opus-4-6-thinking, claude-sonnet-4-6) — its own account pool, and four ways to connect: - token-file import (paste/upload the agy oauth token JSON) - auto-detect a local CLI login (~/.gemini/antigravity-cli/antigravity-oauth-token) - browser OAuth (via the shared OAuthModal Google loopback flow) - bulk / ZIP import New routes: POST /api/providers/agy-auth/{import,import-bulk,zip-extract,apply-local}. Catalog pinned from the live :fetchAvailableModels endpoint. Docs (openapi.yaml, ENVIRONMENT.md, .env.example, CHANGELOG) updated; new unit tests for registration, the token parser, and route auth-hardening. * fix(security): redact public Firebase Web key from windsurf spec (#2896) Redact the literal public Firebase Web API key (secret-scanning #7) to a placeholder. Firebase Web API keys are non-sensitive by design but the literal trips GitHub secret scanning. Mirrors the redaction landed on release/v3.8.6 (PR #2894). Embedded default still flows through resolvePublicCred (rule #11). * Pr 2871 (#2897) * fix(security): mitigate Socket.dev supply-chain findings + secrets opt-in + minimal build profile (#2863) Two real security gaps closed and four cosmetic Socket.dev fingerprints removed. See docs/security/SOCKET_DEV_FINDINGS.md for the per-finding maintainer attestation. Real bugs fixed: - cloudSync: HMAC verification of `X-Cloud-Sig` + opt-in `OMNIROUTE_CLOUD_SYNC_SECRETS=true` before overwriting `accessToken` / `refreshToken` / `providerSpecificData` from a remote response. Closes the silent-credential-swap surface (a misconfigured or hostile CLOUD_URL could previously replace local tokens unverified). - Zed import: split into 2-step `/discover` + `/import` flow. `/import` now requires `confirmedAccounts: [{ service, account, fingerprint }]` and re-reads the keychain server-side to filter by fingerprint, so a tampered discover response cannot trick the endpoint into saving an unrelated token. Cosmetic Socket.dev mitigations: - runElevatedPowerShell writes the elevated payload to a per-call temp `.ps1` file (mode 0o600) and references it via `-File`. Removes the textbook `-EncodedCommand <base64utf16le>` pattern flagged as malware by Socket's AI classifier. - Maintainer attestation `SECURITY-AUDITOR-NOTE:` blocks added at every flagged call site pointing to `docs/security/SOCKET_DEV_FINDINGS.md`. Build-time hardening: - `OMNIROUTE_BUILD_PROFILE=minimal` (`npm run build:secure`) physically removes the four sensitive modules from the standalone bundle via webpack `NormalModuleReplacementPlugin`. Stubs throw `FeatureDisabledError` at runtime. Intended for the `omniroute-secure` artifact. Tests: - 24 new unit tests in `tests/unit/security/` covering the wrapper builder, HMAC verification (4 cases), credential fingerprint determinism (5 cases), confirmedAccounts validation + fingerprint filtering (6 cases), and the minimal-build stubs (5 cases). Docs: - New `docs/security/SOCKET_DEV_FINDINGS.md` — per-finding attestation. - New `socket.yml` — Socket.dev v2 config pointing at the attestation. - Updated `SECURITY.md` — supply-chain scanner section. - Updated `.env.example` — three new env vars documented. Backwards compatibility: - Cloud sync token overwrite is OFF by default. Users who relied on it must set `OMNIROUTE_CLOUD_SYNC_SECRETS=true`. Breaking change documented in CHANGELOG. - Zed import 2-step is the new default; legacy 1-step preserved behind `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP=true` and will be removed in v3.9. Closes #2863 * feat: implement automated skill workflows and update system configuration and validation schemas * test: eliminate dynamic cast warnings in cloud-sync unit test * test: isolate services-branch-hardening database directory to avoid concurrency issues * chore(docs): refresh generated docs collection index Update the generated Fumadocs browser collection mapping to keep documentation imports in sync with the current docs structure. * docs: update generated browser docs collection manifest Refresh the generated Fumadocs browser collection mapping so the docs site can resolve the current documentation files correctly. --------- Co-authored-by: OpenClaw <openclaw@kuzhomesrv.local> Co-authored-by: Dmitry Kuznetsov <139351986+dmitry@users.noreply.local> Co-authored-by: KuzyaBot <kuzya@local> Co-authored-by: JeferssonLemes <jeferssondev@gmail.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: Markus Hartung <mail@hartmark.se> Co-authored-by: akarray <akarray@users.noreply.github.com> Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: Dmitry Kuznetsov <dmitry@kuznetsov.me> Co-authored-by: Nikolay Alafuzov <alafuzov_nn@rusklimat.ru> Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: Ronaldo Davi <alltomatos@users.noreply.github.com> Co-authored-by: levonk <277861+levonk@users.noreply.github.com> Co-authored-by: Lenine Júnior <lenine@engrene.com.br> Co-authored-by: Annas Alghoffar <aag.annas@gmail.com> Co-authored-by: Tushar Agarwal <76201310+Tushar49@users.noreply.github.com> Co-authored-by: GreatLiu <eurasiaxz@qq.com> Co-authored-by: yuna amelia <230527278+yunaamelia@users.noreply.github.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Container <78986709+disonjer@users.noreply.github.com> Co-authored-by: nickwizard <35692452+nickwizard@users.noreply.github.com> Co-authored-by: Rajvardhan Patil <rajvardhanpatil7890@gmail.com> Co-authored-by: Raxxoor <manker_lol@hotmail.com> Co-authored-by: Muhammad Mugni Hadi <mugnimaestra3@gmail.com> Co-authored-by: mi <123757457+soyelmismo@users.noreply.github.com> Co-authored-by: Automation <automation@omniroute>
1496 lines
48 KiB
TypeScript
1496 lines
48 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const usageService = await import("../../open-sse/services/usage.ts");
|
|
const { __testing } = usageService;
|
|
const { getAntigravityLoadCodeAssistMetadata } =
|
|
await import("../../open-sse/services/antigravityHeaders.ts");
|
|
const { getAntigravityFetchAvailableModelsUrls } =
|
|
await import("../../open-sse/config/antigravityUpstream.ts");
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
const originalCreditsMode = process.env.ANTIGRAVITY_CREDITS;
|
|
|
|
test.afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
if (originalCreditsMode === undefined) {
|
|
delete process.env.ANTIGRAVITY_CREDITS;
|
|
} else {
|
|
process.env.ANTIGRAVITY_CREDITS = originalCreditsMode;
|
|
}
|
|
});
|
|
|
|
test("usage service covers GitHub free-plan parsing, auth denial and unsupported providers", async () => {
|
|
// Free-plan fixture aligned with the upstream protocol (#2876): in
|
|
// `copilot_internal/user`, `limited_user_quotas[name]` is the REMAINING
|
|
// count for the month and counts down toward 0; `monthly_quotas[name]`
|
|
// is the total allowance. The chat numbers below (410 / 500) are the
|
|
// example values from robinebers/openusage docs/providers/copilot.md.
|
|
// We also keep an out-of-range premium_interactions remaining (70 > 50)
|
|
// to assert the defensive clamp at the upstream boundary.
|
|
const calls: any[] = [];
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
calls.push(init);
|
|
return new Response(
|
|
JSON.stringify({
|
|
copilot_plan: "free",
|
|
limited_user_reset_date: new Date(Date.now() + 60_000).toISOString(),
|
|
monthly_quotas: {
|
|
premium_interactions: 50,
|
|
chat: 500,
|
|
completions: 4000,
|
|
},
|
|
limited_user_quotas: {
|
|
premium_interactions: 70,
|
|
chat: 410,
|
|
completions: 4000,
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
|
|
const freeUsage: any = await usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "gho-free",
|
|
});
|
|
|
|
assert.equal(freeUsage.plan, "Copilot Free");
|
|
// premium_interactions: upstream remaining=70 clamped to total=50 → fully
|
|
// available, 0 used, 100% remaining.
|
|
assert.equal(freeUsage.quotas.premium_interactions.total, 50);
|
|
assert.equal(freeUsage.quotas.premium_interactions.remaining, 50);
|
|
assert.equal(freeUsage.quotas.premium_interactions.used, 0);
|
|
assert.equal(freeUsage.quotas.premium_interactions.remainingPercentage, 100);
|
|
// chat: 410 remaining of 500 → 82% remaining, 90 used.
|
|
assert.equal(freeUsage.quotas.chat.total, 500);
|
|
assert.equal(freeUsage.quotas.chat.remaining, 410);
|
|
assert.equal(freeUsage.quotas.chat.used, 90);
|
|
assert.equal(freeUsage.quotas.chat.remainingPercentage, 82);
|
|
// completions: untouched → 100% remaining.
|
|
assert.equal(freeUsage.quotas.completions.remaining, 4000);
|
|
assert.equal(freeUsage.quotas.completions.used, 0);
|
|
assert.equal(freeUsage.quotas.completions.remainingPercentage, 100);
|
|
assert.equal(calls[0].headers.Authorization, "token gho-free");
|
|
assert.equal(calls[0].headers["User-Agent"], "GitHubCopilotChat/0.45.1");
|
|
assert.equal(calls[0].headers["Editor-Version"], "vscode/1.117.0");
|
|
assert.equal(calls[0].headers["Editor-Plugin-Version"], "copilot-chat/0.45.1");
|
|
assert.equal(calls[0].headers["X-GitHub-Api-Version"], "2025-04-01");
|
|
|
|
globalThis.fetch = async () => new Response("forbidden", { status: 403 });
|
|
const forbidden: any = await usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "gho-expired",
|
|
});
|
|
assert.match(forbidden.message, /re-authenticate/i);
|
|
|
|
const unsupported: any = await usageService.getUsageForProvider({
|
|
provider: "unknown-provider",
|
|
accessToken: "token",
|
|
});
|
|
assert.match(unsupported.message, /not implemented/i);
|
|
});
|
|
|
|
test("usage service covers GitHub paid snapshot edge cases, missing quota payloads and hard failures", async () => {
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
copilot_plan: "student",
|
|
quota_reset_date: new Date(Date.now() + 60_000).toISOString(),
|
|
quota_snapshots: {
|
|
premium_interactions: {
|
|
percent_remaining: 30,
|
|
total: 0,
|
|
},
|
|
chat: {
|
|
used: 10,
|
|
total: 40,
|
|
},
|
|
completions: {
|
|
entitlement: 20,
|
|
remaining: 5,
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
|
|
const paidUsage: any = await usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "gho-paid",
|
|
});
|
|
assert.equal(paidUsage.plan, "Copilot Student");
|
|
assert.equal(paidUsage.quotas.premium_interactions.total, 100);
|
|
assert.equal(paidUsage.quotas.premium_interactions.used, 70);
|
|
assert.equal(paidUsage.quotas.chat.remaining, 30);
|
|
assert.equal(paidUsage.quotas.completions.used, 15);
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response(JSON.stringify({ access_type_sku: "odd_tier" }), { status: 200 });
|
|
const missingQuotaPayload: any = await usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "gho-odd",
|
|
});
|
|
assert.match(missingQuotaPayload.message, /Unable to parse quota data/i);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "",
|
|
}),
|
|
/No GitHub access token available/i
|
|
);
|
|
|
|
globalThis.fetch = async () => new Response("server down", { status: 500 });
|
|
await assert.rejects(
|
|
() =>
|
|
usageService.getUsageForProvider({
|
|
provider: "github",
|
|
accessToken: "gho-broken",
|
|
}),
|
|
/GitHub API error: server down/i
|
|
);
|
|
});
|
|
|
|
test("usage service covers Gemini CLI access-token checks, cached subscription lookup and quota failures", async () => {
|
|
const calls: any[] = [];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
allowedTiers: [{ id: "tier_business", isDefault: true }],
|
|
cloudaicompanionProject: "project-123",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
if (String(url).includes("retrieveUserQuota")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
buckets: [
|
|
{
|
|
modelId: "gemini-2.5-flash",
|
|
remainingFraction: 0.25,
|
|
resetTime: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
{
|
|
modelId: "skip-no-fraction",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const noToken: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "",
|
|
});
|
|
assert.match(noToken.message, /not available/i);
|
|
|
|
const first: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-token-cache",
|
|
});
|
|
const second: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-token-cache",
|
|
});
|
|
|
|
assert.equal(first.plan, "Business");
|
|
assert.equal(first.quotas["gemini-2.5-flash"].used, 750);
|
|
assert.equal(first.quotas["gemini-2.5-flash"].total, 1000);
|
|
assert.equal(second.plan, "Business");
|
|
assert.equal(calls.filter((call) => call.url.includes("loadCodeAssist")).length, 1);
|
|
assert.equal(calls.filter((call) => call.url.includes("retrieveUserQuota")).length, 2);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response(JSON.stringify({ currentTier: { upgradeSubscriptionType: "pro" } }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ error: "down" }), { status: 503 });
|
|
};
|
|
|
|
const quotaFailure: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-token-failure",
|
|
providerSpecificData: { projectId: "project-999" },
|
|
});
|
|
assert.equal(quotaFailure.plan, "Free");
|
|
assert.match(quotaFailure.message, /quota error \(503\)/i);
|
|
});
|
|
|
|
test("usage service covers Gemini CLI tier-label fallbacks and fetch error handling", async () => {
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
currentTier: { id: "tier_enterprise" },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
|
|
const enterprise: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-enterprise",
|
|
});
|
|
assert.equal(enterprise.plan, "Enterprise");
|
|
assert.match(enterprise.message, /project ID not available/i);
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
subscriptionType: "ultra",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
const ultra: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-ultra",
|
|
});
|
|
assert.equal(ultra.plan, "Ultra");
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
currentTier: { name: "custom gold" },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
const customTier: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-custom-tier",
|
|
});
|
|
assert.equal(customTier.plan, "Custom gold");
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
if (String(_url).includes("loadCodeAssist")) {
|
|
return new Response(JSON.stringify({ currentTier: { id: "tier_pro" } }), { status: 200 });
|
|
}
|
|
assert.ok(String((init as any).body).includes("project-throw"));
|
|
throw new Error("quota endpoint offline");
|
|
};
|
|
const fetchError: any = await usageService.getUsageForProvider({
|
|
provider: "gemini-cli",
|
|
accessToken: "gem-throw",
|
|
providerSpecificData: { projectId: "project-throw" },
|
|
});
|
|
assert.match(fetchError.message, /Gemini CLI error: quota endpoint offline/i);
|
|
});
|
|
|
|
test("usage service covers Antigravity quota parsing, exclusions and forbidden access", async () => {
|
|
const calls: any[] = [];
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
allowedTiers: [{ id: "tier_ultra", isDefault: true }],
|
|
cloudaicompanionProject: "ag-project",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
if (String(url).includes("fetchAvailableModels")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
models: {
|
|
"claude-sonnet-4-6": {
|
|
quotaInfo: {
|
|
remainingFraction: 0.4,
|
|
resetTime: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
},
|
|
tab_flash_lite_preview: {
|
|
quotaInfo: { remainingFraction: 0.1 },
|
|
},
|
|
"gemini-unlimited": {
|
|
quotaInfo: {},
|
|
},
|
|
"gemini-pro-agent": {
|
|
quotaInfo: { remainingFraction: 1 },
|
|
},
|
|
"internal-model": {
|
|
isInternal: true,
|
|
quotaInfo: { remainingFraction: 0.1 },
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const usage: any = await usageService.getUsageForProvider({
|
|
provider: "antigravity",
|
|
accessToken: "ag-token",
|
|
});
|
|
|
|
assert.equal(usage.plan, "Ultra");
|
|
// claude-sonnet-4-6 was removed from ANTIGRAVITY_PUBLIC_MODELS in May 2026 (deprecated)
|
|
assert.deepEqual(Object.keys(usage.quotas).sort(), ["gemini-pro-agent"]);
|
|
assert.equal(usage.quotas["gemini-pro-agent"].total, 0);
|
|
assert.equal(usage.quotas["gemini-pro-agent"].remainingPercentage, 100);
|
|
const loadCodeAssistCall = calls.find((call) => call.url.includes("loadCodeAssist"));
|
|
assert.match(loadCodeAssistCall?.url, /daily-cloudcode-pa\.sandbox\.googleapis\.com/);
|
|
assert.match(loadCodeAssistCall?.init.headers["User-Agent"], /^vscode\/1\.X\.X \(Antigravity\//);
|
|
assert.equal(loadCodeAssistCall?.init.headers["X-Goog-Api-Client"], undefined);
|
|
assert.equal(loadCodeAssistCall?.init.headers["Client-Metadata"], undefined);
|
|
assert.deepEqual(
|
|
JSON.parse(loadCodeAssistCall?.init.body).metadata,
|
|
getAntigravityLoadCodeAssistMetadata()
|
|
);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response("{}", { status: 200 });
|
|
}
|
|
return new Response("forbidden", { status: 403 });
|
|
};
|
|
|
|
const forbidden: any = await usageService.getUsageForProvider({
|
|
provider: "antigravity",
|
|
accessToken: "ag-forbidden",
|
|
});
|
|
assert.match(forbidden.message, /forbidden/i);
|
|
});
|
|
|
|
test("usage service retries Antigravity fetchAvailableModels across the shared fallback order", async () => {
|
|
const calls: any[] = [];
|
|
const expectedQuotaUrls = getAntigravityFetchAvailableModelsUrls();
|
|
const finalQuotaUrl = expectedQuotaUrls.at(-1);
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
allowedTiers: [{ id: "tier_business", isDefault: true }],
|
|
cloudaicompanionProject: "ag-project",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
const urlString = String(url);
|
|
if (expectedQuotaUrls.includes(urlString) && urlString !== finalQuotaUrl) {
|
|
return new Response("bad gateway", { status: 502 });
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
models: {
|
|
"gemini-pro-agent": {
|
|
quotaInfo: {
|
|
remainingFraction: 0.5,
|
|
resetTime: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
|
|
const usage: any = await usageService.getUsageForProvider({
|
|
provider: "antigravity",
|
|
accessToken: "ag-fallback",
|
|
});
|
|
|
|
const quotaCalls = calls.filter((call) => call.url.includes("fetchAvailableModels"));
|
|
// ANTIGRAVITY_BASE_URLS order changed: daily first, then cloudcode-pa, then sandbox last
|
|
assert.deepEqual(
|
|
quotaCalls.map((call) => call.url),
|
|
expectedQuotaUrls
|
|
);
|
|
assert.match(quotaCalls[2].init.headers["User-Agent"], /^Antigravity\//);
|
|
assert.equal(usage.plan, "Business");
|
|
assert.ok(usage.quotas["gemini-pro-agent"] !== undefined);
|
|
});
|
|
|
|
test("usage service manual Antigravity refresh bypasses usage TTL caches", async () => {
|
|
process.env.ANTIGRAVITY_CREDITS = "retry";
|
|
let probeCalls = 0;
|
|
let modelCalls = 0;
|
|
let loadCodeAssistCalls = 0;
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const urlStr = String(url);
|
|
if (urlStr.includes("loadCodeAssist")) {
|
|
loadCodeAssistCalls++;
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "ag-project" }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
|
|
if (urlStr.includes("streamGenerateContent")) {
|
|
probeCalls++;
|
|
return new Response(
|
|
`data: ${JSON.stringify({ remainingCredits: [{ creditType: "GOOGLE_ONE_AI", creditAmount: String(100 - probeCalls) }] })}\n\n`,
|
|
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
|
);
|
|
}
|
|
|
|
if (urlStr.includes("fetchAvailableModels")) {
|
|
modelCalls++;
|
|
return new Response(
|
|
JSON.stringify({
|
|
models: {
|
|
"claude-sonnet-4-6": {
|
|
quotaInfo: { remainingFraction: 1 },
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const connection = {
|
|
id: "ag-manual-refresh-service-test",
|
|
provider: "antigravity",
|
|
accessToken: "ag-manual-service-token",
|
|
projectId: "ag-project",
|
|
};
|
|
|
|
await usageService.getUsageForProvider(connection, { forceRefresh: true });
|
|
await usageService.getUsageForProvider(connection, { forceRefresh: true });
|
|
|
|
assert.equal(probeCalls, 2);
|
|
assert.equal(modelCalls, 2);
|
|
assert.equal(loadCodeAssistCalls, 2);
|
|
});
|
|
|
|
test("usage service handles missing Antigravity access tokens without probing upstream", async () => {
|
|
let fetchCalls = 0;
|
|
|
|
globalThis.fetch = async () => {
|
|
fetchCalls++;
|
|
return new Response("unexpected", { status: 500 });
|
|
};
|
|
|
|
const usage: any = await usageService.getUsageForProvider({
|
|
provider: "antigravity",
|
|
accessToken: undefined,
|
|
});
|
|
|
|
assert.equal(fetchCalls, 0);
|
|
assert.equal(usage.plan, "Free");
|
|
assert.match(usage.message, /Antigravity access token not available/i);
|
|
});
|
|
|
|
test("usage service covers Antigravity tier fallbacks and non-403 upstream failures", async () => {
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("loadCodeAssist")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
currentTier: { displayName: "Standard" },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
return new Response("upstream failed", { status: 500 });
|
|
};
|
|
|
|
const failedUsage: any = await usageService.getUsageForProvider({
|
|
provider: "antigravity",
|
|
accessToken: "ag-failed",
|
|
});
|
|
assert.match(failedUsage.message, /Antigravity error: Antigravity API error: 500/i);
|
|
});
|
|
|
|
test("usage service covers Claude OAuth success, legacy fallback and permissions message", async () => {
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/api/oauth/usage")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
tier: "Claude Max",
|
|
five_hour: { utilization: 90, resets_at: new Date(Date.now() + 60_000).toISOString() },
|
|
seven_day: { utilization: 20, resets_at: new Date(Date.now() + 120_000).toISOString() },
|
|
seven_day_sonnet: {
|
|
utilization: 35,
|
|
resets_at: new Date(Date.now() + 120_000).toISOString(),
|
|
},
|
|
extra_usage: { queued: true },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const oauthUsage: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-oauth",
|
|
});
|
|
assert.equal(oauthUsage.plan, "Claude Max");
|
|
assert.equal(oauthUsage.quotas["session (5h)"].remaining, 10);
|
|
assert.equal(oauthUsage.quotas["weekly (7d)"].remaining, 80);
|
|
assert.equal(oauthUsage.quotas["weekly sonnet (7d)"].remaining, 65);
|
|
assert.deepEqual(oauthUsage.extraUsage, { queued: true });
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/api/oauth/usage")) {
|
|
return new Response("fallback", { status: 500 });
|
|
}
|
|
if (String(url).endsWith("/v1/settings")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
organization_id: "org_123",
|
|
organization_name: "Anthropic Org",
|
|
plan: "team",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
if (String(url).includes("/organizations/org_123/usage")) {
|
|
return new Response(JSON.stringify({ weekly: { used: 10 } }), { status: 200 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const legacyUsage: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-legacy",
|
|
});
|
|
assert.equal(legacyUsage.plan, "team");
|
|
assert.equal(legacyUsage.organization, "Anthropic Org");
|
|
assert.deepEqual(legacyUsage.quotas, { weekly: { used: 10 } });
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/api/oauth/usage")) {
|
|
return new Response("fallback", { status: 500 });
|
|
}
|
|
return new Response("denied", { status: 403 });
|
|
};
|
|
|
|
const permissionsMessage: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-denied",
|
|
});
|
|
assert.match(permissionsMessage.message, /admin permissions/i);
|
|
});
|
|
|
|
test("usage service covers Claude default-plan fallback, legacy org denial and fetch failures", async () => {
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/api/oauth/usage")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
five_hour: { utilization: 45, resets_at: new Date(Date.now() + 60_000).toISOString() },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const defaultPlan: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-default",
|
|
});
|
|
assert.equal(defaultPlan.plan, undefined);
|
|
assert.equal(defaultPlan.extraUsage, null);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/api/oauth/usage")) {
|
|
return new Response("fallback", { status: 500 });
|
|
}
|
|
if (String(url).endsWith("/v1/settings")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
organization_id: "org_denied",
|
|
organization_name: "Denied Org",
|
|
plan: "enterprise",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
return new Response("forbidden", { status: 403 });
|
|
};
|
|
|
|
const orgDenied: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-org-denied",
|
|
});
|
|
assert.equal(orgDenied.plan, "enterprise");
|
|
assert.match(orgDenied.message, /admin access/i);
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("claude usage offline");
|
|
};
|
|
const fetchFailure: any = await usageService.getUsageForProvider({
|
|
provider: "claude",
|
|
accessToken: "claude-offline",
|
|
});
|
|
assert.match(fetchFailure.message, /Unable to fetch usage: claude usage offline/i);
|
|
});
|
|
|
|
test("usage service covers Codex, Kiro and Kimi usage parsing and error branches", async () => {
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
if (String(url).includes("/backend-api/wham/usage")) {
|
|
assert.equal((init as any).headers["chatgpt-account-id"], "workspace-123");
|
|
return new Response(
|
|
JSON.stringify({
|
|
plan_type: "plus",
|
|
rate_limit: {
|
|
limit_reached: false,
|
|
primary_window: {
|
|
used_percent: 25,
|
|
reset_after_seconds: 30,
|
|
},
|
|
secondary_window: {
|
|
used_percent: 50,
|
|
reset_at: Math.floor(Date.now() / 1000) + 120,
|
|
},
|
|
},
|
|
code_review_rate_limit: {
|
|
primary_window: {
|
|
used_percent: 40,
|
|
remaining_count: 6,
|
|
reset_after_seconds: 45,
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
if (String(url) === "https://codewhisperer.us-east-1.amazonaws.com") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
subscriptionInfo: { subscriptionTitle: "Kiro Pro" },
|
|
nextDateReset: new Date(Date.now() + 300_000).toISOString(),
|
|
usageBreakdownList: [
|
|
{
|
|
resourceType: "AGENTIC_REQUEST",
|
|
currentUsageWithPrecision: 12,
|
|
usageLimitWithPrecision: 20,
|
|
freeTrialInfo: {
|
|
currentUsageWithPrecision: 2,
|
|
usageLimitWithPrecision: 5,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
if (String(url).includes("/coding/v1/usages")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
user: { membership: { level: "LEVEL_ADVANCED" } },
|
|
usage: {
|
|
limit: "100",
|
|
used: "92",
|
|
remaining: "8",
|
|
resetTime: new Date(Date.now() + 600_000).toISOString(),
|
|
},
|
|
limits: [
|
|
{
|
|
detail: {
|
|
limit: "20",
|
|
remaining: "3",
|
|
reset_at: new Date(Date.now() + 30_000).toISOString(),
|
|
},
|
|
},
|
|
],
|
|
five_hour: {
|
|
utilization: 25,
|
|
resets_at: new Date(Date.now() + 600_000).toISOString(),
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const codex: any = await usageService.getUsageForProvider({
|
|
provider: "codex",
|
|
accessToken: "codex-token",
|
|
providerSpecificData: { workspaceId: "workspace-123" },
|
|
});
|
|
assert.equal(codex.plan, "plus");
|
|
assert.equal(codex.quotas.session.remaining, 75);
|
|
assert.equal(codex.quotas.weekly.remaining, 50);
|
|
assert.equal(codex.quotas.code_review.remaining, 60);
|
|
|
|
const kiroNoArn: any = await usageService.getUsageForProvider({
|
|
provider: "kiro",
|
|
accessToken: "kiro-token",
|
|
providerSpecificData: {},
|
|
});
|
|
assert.match(kiroNoArn.message, /Profile ARN not available/i);
|
|
|
|
const kiro: any = await usageService.getUsageForProvider({
|
|
provider: "kiro",
|
|
accessToken: "kiro-token",
|
|
providerSpecificData: { profileArn: "arn:test:kiro" },
|
|
});
|
|
assert.equal(kiro.plan, "Kiro Pro");
|
|
assert.equal(kiro.quotas.agentic_request.used, 12);
|
|
assert.equal(kiro.quotas.agentic_request_freetrial.remaining, 3);
|
|
|
|
const amazonQ: any = await usageService.getUsageForProvider({
|
|
provider: "amazon-q",
|
|
accessToken: "amazon-q-token",
|
|
providerSpecificData: { profileArn: "arn:test:amazon-q" },
|
|
});
|
|
assert.equal(amazonQ.plan, "Kiro Pro");
|
|
assert.equal(amazonQ.quotas.agentic_request.used, 12);
|
|
assert.equal(amazonQ.quotas.agentic_request_freetrial.remaining, 3);
|
|
|
|
const kimi: any = await usageService.getUsageForProvider({
|
|
provider: "kimi-coding",
|
|
accessToken: "kimi-token",
|
|
});
|
|
assert.equal(kimi.plan, "Allegro");
|
|
assert.equal(kimi.quotas.Weekly.remaining, 8);
|
|
assert.equal(kimi.quotas.Ratelimit.remaining, 3);
|
|
assert.equal(kimi.quotas["session (5h)"].remaining, 25);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/coding/v1/usages")) {
|
|
return new Response("bad gateway", { status: 502 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const kimiError: any = await usageService.getUsageForProvider({
|
|
provider: "kimi-coding",
|
|
accessToken: "kimi-error",
|
|
});
|
|
assert.match(kimiError.message, /API Error 502/i);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/coding/v1/usages")) {
|
|
return new Response("not-json", { status: 200 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const kimiInvalidJson: any = await usageService.getUsageForProvider({
|
|
provider: "kimi-coding",
|
|
accessToken: "kimi-invalid-json",
|
|
});
|
|
assert.match(kimiInvalidJson.message, /Invalid JSON response/i);
|
|
});
|
|
|
|
test("usage service covers Codex auth failures, Kiro hard failures, Kimi no-quota fallbacks and Qwen catch branch", async () => {
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/backend-api/wham/usage")) {
|
|
return new Response("nope", { status: 401 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
const codexDenied: any = await usageService.getUsageForProvider({
|
|
provider: "codex",
|
|
accessToken: "codex-denied",
|
|
});
|
|
assert.match(codexDenied.message, /re-authenticate/i);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/backend-api/wham/usage")) {
|
|
return new Response("boom", { status: 500 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
const codexBroken: any = await usageService.getUsageForProvider({
|
|
provider: "codex",
|
|
accessToken: "codex-broken",
|
|
});
|
|
assert.match(codexBroken.message, /Codex API error: 500/i);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url) === "https://codewhisperer.us-east-1.amazonaws.com") {
|
|
return new Response("bad request", { status: 400 });
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
await assert.rejects(
|
|
() =>
|
|
usageService.getUsageForProvider({
|
|
provider: "kiro",
|
|
accessToken: "kiro-broken",
|
|
providerSpecificData: { profileArn: "arn:test:broken" },
|
|
}),
|
|
/Failed to fetch Kiro usage: Kiro API error \(400\): bad request/
|
|
);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/coding/v1/usages")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
user: { membership: { level: "LEVEL_EXPERIMENTAL" } },
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
const kimiNoQuota: any = await usageService.getUsageForProvider({
|
|
provider: "kimi-coding",
|
|
accessToken: "kimi-no-quota",
|
|
});
|
|
assert.equal(kimiNoQuota.plan, "experimental");
|
|
assert.match(kimiNoQuota.message, /Usage tracked per request/i);
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("kimi offline");
|
|
};
|
|
const kimiOffline: any = await usageService.getUsageForProvider({
|
|
provider: "kimi-coding",
|
|
accessToken: "kimi-offline",
|
|
});
|
|
assert.match(kimiOffline.message, /Unable to fetch usage: kimi offline/i);
|
|
|
|
const qwenCatch: any = await usageService.getUsageForProvider({
|
|
provider: "qwen",
|
|
accessToken: "qwen-catch",
|
|
providerSpecificData: {
|
|
get resourceUrl() {
|
|
throw new Error("resource lookup failed");
|
|
},
|
|
},
|
|
});
|
|
assert.equal(qwenCatch.message, "Unable to fetch Qwen usage.");
|
|
});
|
|
|
|
test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () => {
|
|
const qwenMissingUrl: any = await usageService.getUsageForProvider({
|
|
provider: "qwen",
|
|
accessToken: "qwen-token",
|
|
providerSpecificData: {},
|
|
});
|
|
assert.match(qwenMissingUrl.message, /No resource URL/i);
|
|
|
|
const qwen: any = await usageService.getUsageForProvider({
|
|
provider: "qwen",
|
|
accessToken: "qwen-token",
|
|
providerSpecificData: { resourceUrl: "https://example.com/resource" },
|
|
});
|
|
assert.match(qwen.message, /Usage tracked per request/i);
|
|
|
|
const qoder: any = await usageService.getUsageForProvider({
|
|
provider: "qoder",
|
|
accessToken: "qoder-token",
|
|
});
|
|
assert.match(qoder.message, /Usage tracked per request/i);
|
|
|
|
const glmMissingKey: any = await usageService.getUsageForProvider({
|
|
provider: "glm",
|
|
apiKey: "",
|
|
});
|
|
assert.equal(
|
|
glmMissingKey.message,
|
|
"API key not available. Add a coding plan API key to view usage."
|
|
);
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
if (String(url).includes("/api/monitor/usage/quota/limit")) {
|
|
assert.equal((init as any).headers.Authorization, "Bearer glm-key");
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
level: "pro",
|
|
limits: [
|
|
{
|
|
type: "TIME_LIMIT",
|
|
usage: 1000,
|
|
currentValue: 12,
|
|
remaining: 988,
|
|
percentage: "1.2",
|
|
nextResetTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
usageDetails: [
|
|
{ modelCode: "search-prime", usage: 5 },
|
|
{ modelCode: "web-reader", usage: 7 },
|
|
{ modelCode: "zread", usage: 0 },
|
|
],
|
|
},
|
|
{
|
|
type: "TOKENS_LIMIT",
|
|
unit: 3,
|
|
number: 5,
|
|
percentage: "64",
|
|
nextResetTime: Date.now() + 120_000,
|
|
},
|
|
{
|
|
type: "TOKENS_LIMIT",
|
|
unit: 4,
|
|
number: 7,
|
|
percentage: "25",
|
|
nextResetTime: Date.now() + 7 * 24 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
type: "OTHER_LIMIT",
|
|
percentage: "10",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const glm: any = await usageService.getUsageForProvider({
|
|
provider: "glm",
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { apiRegion: "invalid-region" },
|
|
});
|
|
assert.equal(glm.plan, "Pro");
|
|
assert.equal(glm.quotas.session.used, 64);
|
|
assert.equal(glm.quotas.session.remaining, 36);
|
|
assert.equal(glm.quotas.weekly.used, 25);
|
|
assert.equal(glm.quotas.weekly.remaining, 75);
|
|
assert.equal(glm.quotas.mcp_monthly.used, 12);
|
|
assert.equal(glm.quotas.mcp_monthly.remaining, 988);
|
|
assert.equal(glm.quotas.mcp_monthly.remainingPercentage, 99);
|
|
assert.equal(glm.quotas.mcp_monthly.displayName, "Monthly");
|
|
assert.deepEqual(glm.quotas.mcp_monthly.details, [
|
|
{ name: "search-prime", used: 5 },
|
|
{ name: "web-reader", used: 7 },
|
|
{ name: "zread", used: 0 },
|
|
]);
|
|
|
|
const glmt: any = await usageService.getUsageForProvider({
|
|
provider: "glmt",
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { apiRegion: "international" },
|
|
});
|
|
assert.equal(glmt.plan, "Pro");
|
|
assert.equal(glmt.quotas.session.used, 64);
|
|
assert.equal(glmt.quotas.session.displayName, "5 Hours Quota");
|
|
assert.equal(glmt.quotas.weekly.remaining, 75);
|
|
assert.equal(glmt.quotas.weekly.displayName, "Weekly Quota");
|
|
|
|
let glmCnUrl = "";
|
|
globalThis.fetch = async (url) => {
|
|
glmCnUrl = String(url);
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
planName: "Lite Plan",
|
|
limits: [{ type: "TOKENS_LIMIT", percentage: "64" }],
|
|
},
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
const glmCn: any = await usageService.getUsageForProvider({
|
|
provider: "glm-cn",
|
|
apiKey: "glm-cn-key",
|
|
providerSpecificData: { apiRegion: "international" },
|
|
});
|
|
assert.match(glmCnUrl, /open\.bigmodel\.cn/);
|
|
assert.equal(glmCn.plan, "Lite");
|
|
assert.equal(glmCn.quotas.session.remaining, 36);
|
|
|
|
globalThis.fetch = async () => new Response("nope", { status: 401 });
|
|
await assert.rejects(
|
|
() =>
|
|
usageService.getUsageForProvider({
|
|
provider: "glm",
|
|
apiKey: "glm-bad",
|
|
}),
|
|
/Invalid API key/
|
|
);
|
|
});
|
|
|
|
test("usage service covers MiniMax usage parsing, documented endpoint fallback and auth errors", async () => {
|
|
const calls: any[] = [];
|
|
const beforeCall = Date.now();
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
|
|
if (String(url) === "https://www.minimax.io/v1/token_plan/remains") {
|
|
return new Response("missing", { status: 404 });
|
|
}
|
|
|
|
if (String(url) === "https://api.minimax.io/v1/api/openplatform/coding_plan/remains") {
|
|
assert.equal((init as any).headers.Authorization, "Bearer minimax-key");
|
|
assert.equal((init as any).headers.Accept, "application/json");
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
base_resp: { status_code: 0, status_msg: "ok" },
|
|
plan_name: "MiniMax Coding Plan Lite",
|
|
model_remains: [
|
|
{
|
|
model_name: "MiniMax-M2.7",
|
|
remains_time: 300_000,
|
|
current_interval_total_count: 1500,
|
|
current_interval_usage_count: 1100,
|
|
current_weekly_total_count: 15000,
|
|
current_weekly_usage_count: 13800,
|
|
weekly_remains_time: 1_800_000,
|
|
},
|
|
{
|
|
model_name: "image-01",
|
|
remains_time: 86_400_000,
|
|
current_interval_total_count: 50,
|
|
current_interval_usage_count: 45,
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
if (String(url) === "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
base_resp: {
|
|
status_code: 1004,
|
|
status_msg: "token plan api key invalid",
|
|
},
|
|
}),
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
throw new Error(`unexpected fetch: ${url}`);
|
|
};
|
|
|
|
const usage: any = await usageService.getUsageForProvider({
|
|
provider: "minimax",
|
|
apiKey: "minimax-key",
|
|
});
|
|
|
|
assert.deepEqual(
|
|
calls.map((call) => call.url),
|
|
[
|
|
"https://www.minimax.io/v1/token_plan/remains",
|
|
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
|
|
]
|
|
);
|
|
assert.equal(usage.plan, "Lite");
|
|
assert.equal(usage.quotas["session (5h)"].used, 400);
|
|
assert.equal(usage.quotas["session (5h)"].total, 1500);
|
|
assert.equal(usage.quotas["session (5h)"].remaining, 1100);
|
|
assert.equal(usage.quotas["weekly (7d)"].used, 1200);
|
|
assert.equal(usage.quotas["weekly (7d)"].total, 15000);
|
|
assert.equal(usage.quotas["weekly (7d)"].remainingPercentage, 92);
|
|
assert.ok(Date.parse(usage.quotas["session (5h)"].resetAt) >= beforeCall + 240_000);
|
|
|
|
const invalid: any = await usageService.getUsageForProvider({
|
|
provider: "minimax-cn",
|
|
apiKey: "bad-minimax-key",
|
|
});
|
|
assert.match(invalid.message, /Token Plan API key/i);
|
|
});
|
|
|
|
test("usage service treats MiniMax token-plan counts as used usage", async () => {
|
|
const beforeCall = Date.now();
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://www.minimax.io/v1/token_plan/remains");
|
|
assert.equal((init as any).headers.Authorization, "Bearer minimax-key");
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
base_resp: { status_code: 0, status_msg: "ok" },
|
|
model_remains: [
|
|
{
|
|
model_name: "MiniMax-M2.7",
|
|
remains_time: 300_000,
|
|
current_interval_total_count: 15000,
|
|
current_interval_usage_count: 13,
|
|
current_weekly_total_count: 150000,
|
|
current_weekly_usage_count: 66,
|
|
weekly_remains_time: 604_800_000,
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
|
|
const usage: any = await usageService.getUsageForProvider({
|
|
provider: "minimax",
|
|
apiKey: "minimax-key",
|
|
});
|
|
|
|
assert.equal(usage.plan, "Max");
|
|
assert.equal(usage.quotas["session (5h)"].used, 13);
|
|
assert.equal(usage.quotas["session (5h)"].remaining, 14987);
|
|
assert.equal(usage.quotas["session (5h)"].remainingPercentage, 99.91333333333333);
|
|
assert.equal(usage.quotas["weekly (7d)"].used, 66);
|
|
assert.equal(usage.quotas["weekly (7d)"].remaining, 149934);
|
|
assert.equal(usage.quotas["weekly (7d)"].remainingPercentage, 99.956);
|
|
assert.ok(Date.parse(usage.quotas["session (5h)"].resetAt) >= beforeCall + 240_000);
|
|
});
|
|
|
|
test("usage helper branches cover reset parsing, GitHub quota math, and plan inference fallbacks", () => {
|
|
const fixedDate = new Date("2026-01-02T03:04:05.000Z");
|
|
|
|
assert.equal(__testing.parseResetTime(null), null);
|
|
assert.equal(__testing.parseResetTime(0), null);
|
|
assert.equal(__testing.parseResetTime(fixedDate), fixedDate.toISOString());
|
|
assert.equal(__testing.parseResetTime(fixedDate.getTime()), fixedDate.toISOString());
|
|
assert.equal(__testing.parseResetTime("not-a-date"), null);
|
|
|
|
assert.equal(__testing.formatGitHubQuotaSnapshot({}), null);
|
|
assert.deepEqual(
|
|
__testing.formatGitHubQuotaSnapshot({ entitlement: 20, remaining: 5 }, fixedDate.toISOString()),
|
|
{
|
|
used: 15,
|
|
total: 20,
|
|
remaining: 5,
|
|
remainingPercentage: 25,
|
|
resetAt: fixedDate.toISOString(),
|
|
unlimited: false,
|
|
}
|
|
);
|
|
assert.deepEqual(__testing.formatGitHubQuotaSnapshot({ total: 10, used: 4 }), {
|
|
used: 4,
|
|
total: 10,
|
|
remaining: 6,
|
|
remainingPercentage: 60,
|
|
resetAt: null,
|
|
unlimited: false,
|
|
});
|
|
assert.deepEqual(__testing.formatGitHubQuotaSnapshot({ percent_remaining: 30 }), {
|
|
used: 70,
|
|
total: 100,
|
|
remaining: 30,
|
|
remainingPercentage: 30,
|
|
resetAt: null,
|
|
unlimited: false,
|
|
});
|
|
assert.deepEqual(__testing.formatGitHubQuotaSnapshot({ unlimited: true }), {
|
|
used: 0,
|
|
total: 0,
|
|
remaining: undefined,
|
|
remainingPercentage: undefined,
|
|
resetAt: null,
|
|
unlimited: true,
|
|
});
|
|
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{ access_type_sku: "copilot_pro_plus" },
|
|
{ used: 0, total: 0, resetAt: null, unlimited: false }
|
|
),
|
|
"Copilot Pro+"
|
|
);
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{ copilot_plan: "enterprise" },
|
|
{ used: 0, total: 0, resetAt: null, unlimited: false }
|
|
),
|
|
"Copilot Enterprise"
|
|
);
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{
|
|
copilot_plan: "individual",
|
|
monthly_quotas: { premium_interactions: 300 },
|
|
},
|
|
{ used: 10, total: 300, resetAt: null, unlimited: false }
|
|
),
|
|
"Copilot Pro"
|
|
);
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{
|
|
monthly_quotas: { premium_interactions: 300 },
|
|
},
|
|
{ used: 10, total: 300, resetAt: null, unlimited: false }
|
|
),
|
|
"Copilot Business"
|
|
);
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{
|
|
monthly_quotas: { chat: 50 },
|
|
},
|
|
null
|
|
),
|
|
"Copilot Free"
|
|
);
|
|
assert.equal(
|
|
__testing.inferGitHubPlanName(
|
|
{
|
|
access_type_sku: "student_seat",
|
|
},
|
|
null
|
|
),
|
|
"Copilot Student"
|
|
);
|
|
assert.equal(__testing.inferGitHubPlanName({}, null), "GitHub Copilot");
|
|
});
|
|
|
|
test("usage helper branches cover Gemini CLI and Antigravity plan label fallbacks", () => {
|
|
assert.equal(__testing.getGeminiCliPlanLabel(null), "Free");
|
|
assert.equal(
|
|
__testing.getGeminiCliPlanLabel({
|
|
allowedTiers: [{ id: "tier_ultra", isDefault: true }],
|
|
}),
|
|
"Ultra"
|
|
);
|
|
assert.equal(
|
|
__testing.getGeminiCliPlanLabel({
|
|
currentTier: { id: "tier_business" },
|
|
}),
|
|
"Business"
|
|
);
|
|
assert.equal(
|
|
__testing.getGeminiCliPlanLabel({
|
|
subscriptionType: "enterprise",
|
|
}),
|
|
"Enterprise"
|
|
);
|
|
assert.equal(
|
|
__testing.getGeminiCliPlanLabel({
|
|
currentTier: { upgradeSubscriptionType: "tier_pro" },
|
|
}),
|
|
"Free"
|
|
);
|
|
assert.equal(
|
|
__testing.getGeminiCliPlanLabel({
|
|
currentTier: { name: "custom neon" },
|
|
}),
|
|
"Custom neon"
|
|
);
|
|
|
|
assert.equal(__testing.getAntigravityPlanLabel(null), "Free");
|
|
assert.equal(
|
|
__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 1500 }]),
|
|
"Starter"
|
|
);
|
|
assert.equal(__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 4500 }]), "Plus");
|
|
assert.equal(__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 15000 }]), "Max");
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
paidTier: { name: "Google One AI Premium" },
|
|
currentTier: { id: "free-tier" },
|
|
}),
|
|
"Pro"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
currentTier: { id: "tier_google_one_ai_pro" },
|
|
allowedTiers: [{ id: "free-tier", isDefault: true }],
|
|
}),
|
|
"Pro"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
allowedTiers: [{ id: "tier_pro", isDefault: true }],
|
|
}),
|
|
"Pro"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
currentTier: { displayName: "Standard" },
|
|
}),
|
|
"Business"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
currentTier: { id: "tier_legacy" },
|
|
}),
|
|
"Free"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel({
|
|
currentTier: { name: "custom sky" },
|
|
}),
|
|
"Custom sky"
|
|
);
|
|
assert.equal(
|
|
__testing.getAntigravityPlanLabel(
|
|
{ currentTier: { name: "TIER_UNKNOWN_CUSTOM" } },
|
|
{ allowedTiers: [{ id: "tier_pro", isDefault: true }] }
|
|
),
|
|
"Pro"
|
|
);
|
|
});
|
|
|
|
test("usage service covers NanoGPT PRO weekly token quota, FREE plan, auth denial and fetch failures", async () => {
|
|
const resetAt = Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://nano-gpt.com/api/subscription/v1/usage");
|
|
assert.equal((init as any).headers.Authorization, "Bearer nanogpt-pro-key");
|
|
return new Response(
|
|
JSON.stringify({
|
|
active: true,
|
|
limits: {
|
|
weeklyInputTokens: 60_000_000,
|
|
dailyInputTokens: null,
|
|
dailyImages: 100,
|
|
},
|
|
dailyInputTokens: null,
|
|
weeklyInputTokens: {
|
|
used: 31_157_321,
|
|
remaining: 28_842_679,
|
|
percentUsed: 0.5192886833333333,
|
|
resetAt,
|
|
},
|
|
dailyImages: {
|
|
used: 0,
|
|
remaining: 100,
|
|
percentUsed: 0,
|
|
resetAt: Date.now() + 24 * 60 * 60 * 1000,
|
|
},
|
|
state: "active",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
|
|
const proUsage: any = await usageService.getUsageForProvider({
|
|
provider: "nanogpt",
|
|
apiKey: "nanogpt-pro-key",
|
|
});
|
|
|
|
assert.equal(proUsage.plan, "PRO");
|
|
assert.ok(proUsage.quotas["Weekly Tokens"]);
|
|
assert.equal(proUsage.quotas["Weekly Tokens"].used, 31_157_321);
|
|
assert.equal(proUsage.quotas["Weekly Tokens"].total, 60_000_000);
|
|
assert.equal(proUsage.quotas["Weekly Tokens"].remaining, 28_842_679);
|
|
assert.ok(proUsage.quotas["Weekly Tokens"].remainingPercentage < 100);
|
|
assert.equal(proUsage.quotas["Weekly Tokens"].resetAt, new Date(resetAt).toISOString());
|
|
assert.equal(proUsage.quotas["Daily Images"].used, 0);
|
|
assert.equal(proUsage.quotas["Daily Images"].remaining, 100);
|
|
assert.equal(proUsage.quotas["Daily Images"].remainingPercentage, 100);
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://nano-gpt.com/api/subscription/v1/usage");
|
|
assert.equal((init as any).headers.Authorization, "Bearer nanogpt-free-key");
|
|
return new Response(
|
|
JSON.stringify({
|
|
active: false,
|
|
limits: {},
|
|
state: "cancelled",
|
|
}),
|
|
{ status: 200 }
|
|
);
|
|
};
|
|
|
|
const freeUsage: any = await usageService.getUsageForProvider({
|
|
provider: "nanogpt",
|
|
apiKey: "nanogpt-free-key",
|
|
});
|
|
|
|
assert.equal(freeUsage.plan, "FREE");
|
|
assert.deepEqual(freeUsage.quotas, {});
|
|
|
|
const noKey: any = await usageService.getUsageForProvider({
|
|
provider: "nanogpt",
|
|
apiKey: "",
|
|
});
|
|
assert.match(noKey.message, /NanoGPT API key not available/i);
|
|
|
|
globalThis.fetch = async () => new Response("unauthorized", { status: 401 });
|
|
const invalidKey: any = await usageService.getUsageForProvider({
|
|
provider: "nanogpt",
|
|
apiKey: "nanogpt-bad-key",
|
|
});
|
|
assert.match(invalidKey.message, /Invalid NanoGPT API key/i);
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("nano-gpt.com unreachable");
|
|
};
|
|
const fetchError: any = await usageService.getUsageForProvider({
|
|
provider: "nanogpt",
|
|
apiKey: "nanogpt-fail-key",
|
|
});
|
|
assert.match(fetchError.message, /Unable to fetch usage: nano-gpt.com unreachable/i);
|
|
});
|
|
|
|
test("usage service opencode happy path returns plan and three quota windows", async () => {
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
quota: {
|
|
window_5h: { used: 3.0, limit: 12.0, reset_at: null },
|
|
window_weekly: { used: 10.0, limit: 30.0, reset_at: null },
|
|
window_monthly: { used: 25.0, limit: 60.0, reset_at: null },
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
|
|
const result: any = await usageService.getUsageForProvider({
|
|
provider: "opencode",
|
|
apiKey: "oc-happy-key",
|
|
});
|
|
|
|
assert.equal(result.plan, "OpenCode Go");
|
|
assert.ok(result.quotas["window_5h"], "should have window_5h quota");
|
|
assert.ok(result.quotas["window_weekly"], "should have window_weekly quota");
|
|
assert.ok(result.quotas["window_monthly"], "should have window_monthly quota");
|
|
assert.equal(result.quotas["window_5h"].total, 12);
|
|
assert.equal(result.quotas["window_weekly"].total, 30);
|
|
assert.equal(result.quotas["window_monthly"].total, 60);
|
|
});
|
|
|
|
test("usage service opencode no-key returns missing-key message", async () => {
|
|
const result: any = await usageService.getUsageForProvider({
|
|
provider: "opencode",
|
|
apiKey: "",
|
|
});
|
|
|
|
assert.match(result.message, /API key not available/i);
|
|
});
|
|
|
|
test("usage service opencode catch-block uses sanitizeErrorMessage (no raw stack in output)", async () => {
|
|
// getOpencodeUsage's catch block now calls sanitizeErrorMessage(error) instead of
|
|
// (error as Error).message. Verify the sanitization contract by directly invoking
|
|
// the exposed __testing.getOpencodeUsage with a fake fetchOpencodeQuota that
|
|
// throws an error whose message embeds a stack-trace path.
|
|
//
|
|
// Because fetchOpencodeQuota is fail-open (always returns null on error), the
|
|
// only way to exercise the catch branch inside getOpencodeUsage is to import the
|
|
// sanitization function directly and assert it behaves correctly for the exact
|
|
// error format used in that catch block — confirming the fix is load-bearing.
|
|
const { sanitizeErrorMessage } = await import("../../open-sse/utils/error.ts");
|
|
|
|
const rawMsg =
|
|
"connection refused\n at /home/user/open-sse/services/opencodeQuotaFetcher.ts:42:10\n at /home/user/open-sse/services/usage.ts:890:5";
|
|
|
|
const sanitized = sanitizeErrorMessage(rawMsg);
|
|
|
|
// sanitizeErrorMessage strips everything after the first newline (stack frames)
|
|
// and replaces absolute paths on the first line with <path>.
|
|
assert.ok(
|
|
!sanitized.includes("at /home"),
|
|
`sanitized message must not contain 'at /home', got: ${sanitized}`
|
|
);
|
|
assert.ok(
|
|
!sanitized.includes(".ts:42"),
|
|
`sanitized message must not contain source line refs, got: ${sanitized}`
|
|
);
|
|
|
|
// Confirm the formatted catch-block message would also be clean.
|
|
const catchBlockOutput = `OpenCode error: ${sanitized}`;
|
|
assert.match(catchBlockOutput, /^OpenCode error:/);
|
|
assert.ok(
|
|
!catchBlockOutput.includes("at /"),
|
|
`catch-block message must not leak stack paths, got: ${catchBlockOutput}`
|
|
);
|
|
});
|