mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
feat(api): return every connection's snapshot under providers[] in om-usage json (#11192)
* feat(api): structured ?format=json for the self-service usage endpoint GET /api/usage/om-usage already let any key read its own usage — personal daily/weekly USD limits and the provider quota snapshot — but only as text/plain, which a UI cannot parse safely. OmniCopilot issue #8 asks exactly for this surface. Adds ?format=json, returning the ApiKeyUsageLimitStatus + UsageSnapshot the text is rendered from. Text and JSON share the same collectors (collectUsageSnapshots, getApiKeyUsageLimitStatus), so the two can never disagree about a number. The response is a discriminated union: a key without allowUsageCommand (403) or an invalid key (401) returns { allowed:false, error:{message} }, distinct from allowed:true with empty sections — the state a panel must render as "nothing learned yet", not a refusal. Text form unchanged; without ?format the contract is untouched. The endpoint was previously missing from API_REFERENCE.md; it now has a section documenting both forms, the allowUsageCommand gate, and the self-service auth model (caller's own key, not requireManagementAuth). Regression guards in tests/unit/usage-command-json-format.test.ts (4 tests: json shape, text default preserved, structured 403, sanitized 401 with no stack trace). Existing internal-usage-command suite still 12/12. * chore(changelog): correct the fragment to the real PR number (#11190) * feat(api): return every connection's snapshot under providers[] in om-usage json Closes #11191. buildUsageCommandJson picked a single snapshot via selectUsageSnapshot, so a panel could only ever show one provider. The collector already had them all — the single-pick is a presentation choice for a terminal. The JSON form now also returns the full UsageSnapshot[] alongside the selected provider, so a UI can render Codex / Claude / OpenCode side by side. The text form is untouched. --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me>
This commit is contained in:
committed by
GitHub
parent
eb9fa33ee7
commit
3ef54fc55b
@@ -0,0 +1 @@
|
||||
- **feat(api):** `/api/usage/om-usage?format=json` now returns `providers[]` — every connection's quota snapshot, not just the single selected one — so a panel can render Codex / Claude / OpenCode side by side. The collector already gathered all of them; the single-pick `provider` field (kept) is a terminal presentation choice. Closes the per-connection gap from OmniCopilot #8 ([#11192](https://github.com/diegosouzapw/OmniRoute/pull/11192))
|
||||
@@ -663,7 +663,9 @@ refusal. On success:
|
||||
// present only when the key opted into per-key usage limits (daily/weekly USD):
|
||||
"personal": { "dailySpentUsd": 1.25, "dailyLimitUsd": 5, "dailyResetAtIso": "…", "weeklySpentUsd": 8, "weeklyLimitUsd": 20, "weeklyResetAtIso": "…" /* … */ },
|
||||
// the selected provider quota snapshot, or null when nothing is cached yet:
|
||||
"provider": { "connectionId": "…", "provider": "claude", "plan": "…", "quotas": { /* … */ } }
|
||||
"provider": { "connectionId": "…", "provider": "claude", "plan": "…", "quotas": { /* … */ } },
|
||||
// every connection's snapshot, so a UI can render several providers side by side:
|
||||
"providers": [ { "connectionId": "…", "provider": "claude", /* … */ }, { "provider": "codex", /* … */ } ]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -546,6 +546,11 @@ export type UsageCommandJson =
|
||||
personal: unknown | null;
|
||||
/** The selected provider snapshot, or null when nothing is cached. */
|
||||
provider: UsageSnapshot | null;
|
||||
/** Every connection's snapshot, so a panel can render Codex / Claude /
|
||||
* OpenCode side by side instead of only the selected one (#11191). The
|
||||
* single-pick in `provider` is a presentation choice for a terminal; the
|
||||
* collector already gathered all of them. */
|
||||
providers: UsageSnapshot[];
|
||||
};
|
||||
|
||||
export async function buildUsageCommandJson(
|
||||
@@ -564,11 +569,9 @@ export async function buildUsageCommandJson(
|
||||
{ now: resolvedDeps.now }
|
||||
)
|
||||
: null;
|
||||
const provider = selectUsageSnapshot(
|
||||
await collectUsageSnapshots(metadata, resolvedDeps),
|
||||
selection
|
||||
);
|
||||
return { allowed: true, personal, provider };
|
||||
const snapshots = await collectUsageSnapshots(metadata, resolvedDeps);
|
||||
const provider = selectUsageSnapshot(snapshots, selection);
|
||||
return { allowed: true, personal, provider, providers: snapshots };
|
||||
}
|
||||
|
||||
export async function buildUsageCommandText(
|
||||
|
||||
@@ -40,6 +40,7 @@ function allowedDeps(overrides: Record<string, unknown> = {}) {
|
||||
}),
|
||||
getProviderConnections: async () => [
|
||||
{ id: "conn-claude", provider: "claude", isActive: true },
|
||||
{ id: "conn-codex", provider: "codex", isActive: true },
|
||||
],
|
||||
getAllProviderLimitsCache: () => ({
|
||||
"conn-claude": {
|
||||
@@ -50,6 +51,14 @@ function allowedDeps(overrides: Record<string, unknown> = {}) {
|
||||
message: null,
|
||||
fetchedAt: new Date(NOW).toISOString(),
|
||||
},
|
||||
"conn-codex": {
|
||||
plan: "Codex Pro",
|
||||
quotas: {
|
||||
weekly: { used: 9, total: 100, remaining: 91, resetAt: "2026-08-24T03:00:00.000Z" },
|
||||
},
|
||||
message: null,
|
||||
fetchedAt: new Date(NOW).toISOString(),
|
||||
},
|
||||
}),
|
||||
getProviderConnectionById: async () => null,
|
||||
getProviderLimitsCache: () => null,
|
||||
@@ -95,6 +104,29 @@ test("om-usage without ?format stays text/plain (the historical contract)", asyn
|
||||
assert.match(text, /Provider quota/);
|
||||
});
|
||||
|
||||
test("om-usage ?format=json returns every connection under providers[], not just the selected one", async () => {
|
||||
// #11191 — a panel needs Codex + Claude side by side; the single `provider`
|
||||
// pick is a terminal presentation choice, the collector had them all.
|
||||
const response = await handleInternalUsageCommandHttpRequest(
|
||||
new Request("http://localhost/api/usage/om-usage?format=json", {
|
||||
headers: { Authorization: "Bearer sk-allowed" },
|
||||
}),
|
||||
allowedDeps()
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as {
|
||||
allowed: boolean;
|
||||
provider: { provider: string } | null;
|
||||
providers: Array<{ provider: string }>;
|
||||
};
|
||||
assert.equal(body.allowed, true);
|
||||
const names = body.providers.map((s) => s.provider).sort();
|
||||
assert.deepEqual(names, ["claude", "codex"]);
|
||||
// the single-pick field is still present and one of them
|
||||
assert.ok(["claude", "codex"].includes(body.provider?.provider ?? ""));
|
||||
});
|
||||
|
||||
test("om-usage ?format=json reports a disallowed key as structured allowed:false", async () => {
|
||||
// A usage panel must tell "this key may not ask" apart from "no data yet",
|
||||
// which a bare 403 text body cannot express.
|
||||
|
||||
Reference in New Issue
Block a user