Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 3 additions & 1 deletion docs/reference/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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", /* … */ } ]
}
```

Expand Down
13 changes: 8 additions & 5 deletions src/lib/usage/internalUsageCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/usage-command-json-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Loading