feat: implement the feature for checking quota of orbit provider

This commit is contained in:
2026-07-17 16:43:15 +07:00
parent 7ff4ebeed1
commit 2e595627d0
6 changed files with 288 additions and 0 deletions
@@ -16,6 +16,9 @@ export default {
headers: {
"anthropic-version": "2023-06-01",
},
usage: {
url: "https://api.orbit-provider.com/v1/usage",
},
},
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
@@ -24,6 +27,10 @@ export default {
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)" },
],
serviceKinds: ["llm"],
features: {
usage: true,
usageApikey: true,
},
thinkingConfig: {
options: ["auto", "on", "off"],
defaultMode: "auto",
+2
View File
@@ -12,6 +12,7 @@ import { getKiroUsage } from "./usage/kiro.js";
import { getMiniMaxUsage } from "./usage/minimax.js";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js";
import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getOrbitUsage } from "./usage/orbit.js";
import {
getQwenUsage,
getIflowUsage,
@@ -45,6 +46,7 @@ const USAGE_HANDLERS = {
"vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions),
"codebuddy-cn": (c) => getCodeBuddyCnUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions),
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
"orbit-provider": (c) => getOrbitUsage(c.apiKey, c.proxyOptions),
};
export async function getUsageForProvider(connection, proxyOptions = null) {
+118
View File
@@ -0,0 +1,118 @@
/**
* Orbit Provider usage handler.
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U, parseResetTime, toFiniteNumber } from "./shared.js";
const ORBIT_USAGE_URL = U("orbit-provider").url;
function clampPercentage(value) {
return Math.min(100, Math.max(0, value));
}
function getRemainingPercentage(used, total, upstreamUsagePercent) {
if (total <= 0) return 0;
const usagePercent = Number.isFinite(Number(upstreamUsagePercent))
? toFiniteNumber(upstreamUsagePercent)
: (used / total) * 100;
return clampPercentage(100 - usagePercent);
}
/**
* Normalize Orbit's usage response into quota rows consumed by the dashboard.
* @param {object} responseBody Orbit API response.
* @returns {object} Normalized provider usage.
*/
export function parseOrbitUsage(responseBody) {
if (responseBody?.success !== true || !responseBody?.data || typeof responseBody.data !== "object") {
return { message: "Orbit Provider usage response was invalid." };
}
const data = responseBody.data;
const used = Math.max(0, toFiniteNumber(data.tokensUsed));
const total = Math.max(0, toFiniteNumber(data.tokenLimit));
const upstreamRemaining = Math.max(0, toFiniteNumber(data.tokensRemaining, Math.max(0, total - used)));
const remaining = data.isExhausted === true ? 0 : Math.min(total, upstreamRemaining);
const remainingPercentage = data.isExhausted === true
? 0
: getRemainingPercentage(used, total, data.usagePercent);
const resetPeriod = typeof data.resetPeriod === "string" && data.resetPeriod.trim()
? data.resetPeriod.trim().toLowerCase()
: "period";
const resetAt = parseResetTime(data.periodEnd);
const quotas = {
[`Tokens (${resetPeriod})`]: {
used,
total,
remaining,
remainingPercentage,
resetAt,
unlimited: false,
},
};
const credit = data.credit;
if (credit && typeof credit === "object") {
const balance = Math.max(0, toFiniteNumber(credit.balanceUsd));
const granted = Math.max(0, toFiniteNumber(credit.grantedUsd));
const spent = Math.max(0, toFiniteNumber(credit.spentUsd));
const creditTotal = granted > 0 ? granted : balance + spent;
const creditRemainingPercentage = creditTotal > 0
? clampPercentage((balance / creditTotal) * 100)
: 0;
quotas["Credit (USD)"] = {
used: spent,
total: creditTotal,
remaining: balance,
remainingPercentage: creditRemainingPercentage,
resetAt: null,
unlimited: false,
};
}
return {
plan: typeof data.plan === "string" && data.plan.trim() ? data.plan.trim() : "Unknown",
quotas,
isExhausted: data.isExhausted === true,
resetPeriod,
};
}
/**
* Fetch usage for an Orbit Provider API-key connection.
* @param {string} apiKey Orbit API key.
* @param {object|null} proxyOptions Connection proxy configuration.
* @returns {Promise<object>} Normalized usage data.
*/
export async function getOrbitUsage(apiKey, proxyOptions = null) {
if (!apiKey) {
return { message: "Orbit Provider API key not available." };
}
try {
const response = await proxyAwareFetch(ORBIT_USAGE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
}, proxyOptions);
if (response.status === 401 || response.status === 403) {
return { message: "Orbit Provider API key invalid or expired." };
}
if (!response.ok) {
const errorText = await response.text().catch(() => "");
const detail = errorText ? `: ${errorText.slice(0, 200)}` : "";
return { message: `Orbit Provider usage API error (${response.status})${detail}` };
}
const body = await response.json().catch(() => null);
return parseOrbitUsage(body);
} catch (error) {
return { message: `Orbit Provider usage error: ${error.message}` };
}
}
@@ -492,6 +492,24 @@ export function parseQuotaData(provider, data) {
}
break;
case "orbit-provider":
// Orbit exposes both token usage and USD credit balance, but only the
// credit balance is useful in the quota dashboard. Keep token usage
// in the server response for diagnostics without rendering its row.
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]) => {
if (name !== "Credit (USD)") return;
normalizedQuotas.push({
name,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
}
break;
default:
// Generic fallback for unknown providers
if (data.quotas) {
+142
View File
@@ -0,0 +1,142 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
import { parseOrbitUsage } from "../../open-sse/services/usage/orbit.js";
import { PROVIDERS } from "../../open-sse/providers/index.js";
import { USAGE_APIKEY_PROVIDERS, USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.js";
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
const ORBIT_USAGE_RESPONSE = {
success: true,
data: {
plan: "starter",
usagePercent: 100,
isExhausted: true,
resetPeriod: "monthly",
periodEnd: null,
tokensUsed: 57288213,
tokenLimit: 50000000,
tokensRemaining: 0,
daily: null,
credit: {
balanceMicroUsd: 397891136,
balanceUsd: 397.891136,
balanceUsdFormatted: "$397.89",
grantedMicroUsd: 500000000,
grantedUsd: 500,
spentMicroUsd: 102108864,
spentUsd: 102.108864,
currency: "USD",
source: "ledger",
},
},
};
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("Orbit Provider usage registry", () => {
it("exposes its usage endpoint and API-key eligibility", () => {
expect(PROVIDERS["orbit-provider"].usage?.url).toBe(
"https://api.orbit-provider.com/v1/usage",
);
expect(USAGE_SUPPORTED_PROVIDERS).toContain("orbit-provider");
expect(USAGE_APIKEY_PROVIDERS).toContain("orbit-provider");
});
});
describe("parseOrbitUsage", () => {
it("normalizes exhausted monthly tokens and USD credit", () => {
const usage = parseOrbitUsage(ORBIT_USAGE_RESPONSE);
expect(usage).toMatchObject({
plan: "starter",
isExhausted: true,
resetPeriod: "monthly",
});
expect(usage.quotas["Tokens (monthly)"]).toMatchObject({
used: 57288213,
total: 50000000,
remaining: 0,
remainingPercentage: 0,
resetAt: null,
});
expect(usage.quotas["Credit (USD)"]).toMatchObject({
used: 102.108864,
total: 500,
remaining: 397.891136,
});
expect(usage.quotas["Credit (USD)"].remainingPercentage).toBeCloseTo(79.5782272);
});
it("rejects an unsuccessful or malformed payload", () => {
expect(parseOrbitUsage({ success: false })).toEqual({
message: "Orbit Provider usage response was invalid.",
});
});
});
describe("getUsageForProvider(orbit-provider)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("fetches Orbit usage with the connection API key", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(ORBIT_USAGE_RESPONSE));
const proxyOptions = { connectionProxyEnabled: true };
const usage = await getUsageForProvider({
provider: "orbit-provider",
apiKey: "test-orbit-key",
}, proxyOptions);
expect(usage.plan).toBe("starter");
expect(usage.quotas["Tokens (monthly)"].remainingPercentage).toBe(0);
expect(proxyAwareFetch).toHaveBeenCalledWith(
"https://api.orbit-provider.com/v1/usage",
expect.objectContaining({
method: "GET",
headers: {
Authorization: "Bearer test-orbit-key",
Accept: "application/json",
},
}),
proxyOptions,
);
});
it("returns an authentication message for an invalid API key", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
const usage = await getUsageForProvider({
provider: "orbit-provider",
apiKey: "invalid",
});
expect(usage.message).toMatch(/invalid or expired/i);
});
});
describe("parseQuotaData(orbit-provider)", () => {
it("renders only the credit progress bar", () => {
const usage = parseOrbitUsage(ORBIT_USAGE_RESPONSE);
const rows = parseQuotaData("orbit-provider", usage);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
name: "Credit (USD)",
used: 102.108864,
total: 500,
});
expect(rows[0].remainingPercentage).toBeCloseTo(79.5782272);
});
});
+1
View File
@@ -16,6 +16,7 @@ const SUPPORTED = [
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
"qoder", "qwen", "iflow", "ollama", "glm", "glm-cn",
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli",
"orbit-provider",
];
describe("usage dispatch", () => {