) : (
breakdownModels.map((model, index) => {
- const key = `${model.provider}:${model.model}`;
+ const key = `${model.provider}:${model.upstreamProvider ?? ""}:${model.model}`;
const value = metric === "tokens" ? model.totalTokens : model.costUsd;
const hasBar = value > 0 && breakdownPeak > 0;
const share = modelShare(model, metric === "tokens" ? "tokens" : "cost");
@@ -909,6 +913,11 @@ function UsageOverview({
>
{model.model}
+ {model.upstreamProvider ? (
+
+ {model.upstreamProvider}
+
+ ) : null}
{hasBar ? (
diff --git a/apps/web/src/state/usage.ts b/apps/web/src/state/usage.ts
index bf5de2e330a3f..2da774c4cb129 100644
--- a/apps/web/src/state/usage.ts
+++ b/apps/web/src/state/usage.ts
@@ -98,14 +98,11 @@ export function mergeAnsweredUsage(
{
environmentId,
label,
- summary:
- keepBucket === undefined
- ? summary
- : { ...summary, buckets: summary.buckets.filter(keepBucket) },
+ summary,
},
],
);
- return mergeUsage(answered, USAGE_CONTRACT_VERSION);
+ return mergeUsage(answered, USAGE_CONTRACT_VERSION, keepBucket);
}
const NO_HIDDEN_PROVIDERS: ReadonlySet = new Set();
diff --git a/docs/user/usage.md b/docs/user/usage.md
index e6b3a46d5a14e..6512ec0772542 100644
--- a/docs/user/usage.md
+++ b/docs/user/usage.md
@@ -14,6 +14,8 @@ cost, split by token type and by speed. These estimates are not your subscriptio
**Premium** is what Fast and Ultrafast requests cost above standard rates. Cost that cannot be
split, such as a provider-reported cost for a model without public rates, shows as **Other**.
Select a model under **Breakdown** to see its trend, cache hit rate, and cost per million tokens.
+An OpenCode model shows the upstream that served it, so the same model billed by OpenCode Go and
+by another provider is listed separately.
Totals depend on the history available on each server. Grok turns without a saved completed-turn
record are missing from the totals.
@@ -155,7 +157,12 @@ the environment. T3 cannot report limits for external OpenCode servers because t
belong to the remote server. Limits need an OpenCode Go API key, or an OpenCode Console connected
in OpenCode with an API key. A Console browser sign-in alone does not report them. You can also add
your Go API key as `OPENCODE_API_KEY` in the OpenCode instance's **Environment variables**, then
-refresh provider status. Cursor reports
+refresh provider status.
+An OpenCode account on a MiniMax Token Plan reports that plan's interval and weekly allowance
+next to the Go budget. It needs its own key: sign in to OpenCode with the MiniMax Token Plan and
+store the plan key there. T3 reads the plan's quota only when OpenCode resolves that provider to
+MiniMax's own endpoint, so a provider pointed at another endpoint reports no plan limits.
+Cursor reports
its monthly allowance, including separate Auto and API usage, for `CURSOR_AUTH_TOKEN` when it is set,
otherwise for the account you signed in with in T3 or set as `CURSOR_API_KEY`.
Sign-in and API keys report limits only with Cursor's default API endpoint. If you configure a custom
diff --git a/packages/contracts/src/providerUsageLimits.ts b/packages/contracts/src/providerUsageLimits.ts
index 851de4c72ffe3..e99c35733fd05 100644
--- a/packages/contracts/src/providerUsageLimits.ts
+++ b/packages/contracts/src/providerUsageLimits.ts
@@ -17,6 +17,8 @@ export const ServerProviderUsageWindow = Schema.Struct({
/** When present, this limit applies only to these provider model IDs. */
modelIds: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
providerIds: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
+ /** The credential that meters this window; a pool counts one credential once. */
+ credentialFingerprint: Schema.optional(TrimmedNonEmptyString),
usedPercent: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
resetsAt: Schema.optional(IsoDateTime),
windowDurationMins: Schema.optional(NonNegativeInt),
diff --git a/packages/contracts/src/usage.ts b/packages/contracts/src/usage.ts
index 76dd69ff91699..ae6ce0b42decb 100644
--- a/packages/contracts/src/usage.ts
+++ b/packages/contracts/src/usage.ts
@@ -3,7 +3,7 @@ import * as Schema from "effect/Schema";
import { ForwardCompatibleArray, NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
/** Bump for incompatible UsageSummary changes; skipped unknown array variants need no bump. */
-export const USAGE_CONTRACT_VERSION = 6 as const;
+export const USAGE_CONTRACT_VERSION = 7 as const;
export const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const;
@@ -55,6 +55,8 @@ export const UsageBucket = Schema.Struct({
hourStart: Schema.optional(TrimmedNonEmptyString),
provider: UsageProviderKind,
model: TrimmedNonEmptyString,
+ /** The upstream integration that served the model, when the harness knows it. */
+ upstreamProvider: Schema.optional(TrimmedNonEmptyString),
sourcePath: Schema.optional(TrimmedNonEmptyString),
totals: UsageTokenTotals,
costUsd: Schema.Number,
diff --git a/packages/provider-core/src/server/usage.ts b/packages/provider-core/src/server/usage.ts
index c2c75a641e53a..81351a8d3e22b 100644
--- a/packages/provider-core/src/server/usage.ts
+++ b/packages/provider-core/src/server/usage.ts
@@ -13,6 +13,13 @@ export type UsageSpeed = "standard" | "fast" | "ultrafast";
export interface UsageRecord {
readonly provider: UsageProviderKind;
+ /**
+ * The upstream integration that served the model, when the harness reports
+ * one. A provider harness can front several upstreams (OpenCode fronts
+ * OpenCode Go, Zen, MiniMax and others), and only this names which one a
+ * record belongs to.
+ */
+ readonly upstreamProvider?: string;
readonly timestampMs: number;
readonly model: string;
readonly rateModel?: string;
diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts
index b3c628e28fc63..e02d13c49e9bc 100644
--- a/packages/provider-opencode/src/server/driver.test.ts
+++ b/packages/provider-opencode/src/server/driver.test.ts
@@ -88,14 +88,42 @@ const layer = Layer.mergeAll(
Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, openCode2Runtime),
).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)));
-const create = (config: Partial, http: HttpClient.HttpClient, enabled = true) =>
- OpenCodeDriver.create({
- instanceId: ProviderInstanceId.make("opencode-test"),
- displayName: undefined,
- environment: [],
- enabled,
- config: { ...OpenCodeDriver.defaultConfig(), ...config },
- }).pipe(Effect.provideService(HttpClient.HttpClient, http));
+/**
+ * Keeps the provider from reading the developer's own OpenCode store: the
+ * quota probe would send those credentials to the provider's API.
+ */
+const sandboxStore = Effect.gen(function* () {
+ const fileSystem = yield* FileSystem.FileSystem;
+ const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-opencode-driver-home-" });
+ return [
+ { name: "HOME", value: home, sensitive: false },
+ { name: "XDG_DATA_HOME", value: home, sensitive: false },
+ ];
+});
+
+const create = (
+ config: Partial,
+ http: HttpClient.HttpClient,
+ options: {
+ readonly enabled?: boolean;
+ readonly environment?: NodeJS.ProcessEnv;
+ } = {},
+) =>
+ Effect.gen(function* () {
+ const sandbox = yield* sandboxStore;
+ return yield* OpenCodeDriver.create({
+ instanceId: ProviderInstanceId.make("opencode-test"),
+ displayName: undefined,
+ environment: [
+ ...sandbox,
+ ...Object.entries(options.environment ?? {}).flatMap(([name, value]) =>
+ value === undefined ? [] : [{ name, value, sensitive: false }],
+ ),
+ ],
+ enabled: options.enabled ?? true,
+ config: { ...OpenCodeDriver.defaultConfig(), ...config },
+ }).pipe(Effect.provideService(HttpClient.HttpClient, http));
+ });
const noHttp = HttpClient.make(() => Effect.die("A local binary must not be probed over HTTP"));
@@ -219,7 +247,7 @@ it.live("discovers models through a home-relative OpenCode wrapper", () =>
const instance = yield* OpenCodeDriver.create({
instanceId: ProviderInstanceId.make("opencode-home"),
displayName: undefined,
- environment: [],
+ environment: yield* sandboxStore,
enabled: true,
config: {
...OpenCodeDriver.defaultConfig(),
@@ -326,9 +354,9 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => {
return { stdout: "opencode v2.1.2\n", stderr: "", code: 0 };
}),
} as OpenCodeRuntime.OpenCodeRuntime["Service"];
- const instance = yield* create({ serverUrl: "http://127.0.0.1:4096" }, noHttp, false).pipe(
- Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime),
- );
+ const instance = yield* create({ serverUrl: "http://127.0.0.1:4096" }, noHttp, {
+ enabled: false,
+ }).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime));
const snapshot = yield* instance.snapshot.refresh;
assert.isFalse(snapshot.enabled);
@@ -970,6 +998,7 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => {
instanceId: ProviderInstanceId.make("opencode-usage-recovery"),
displayName: undefined,
environment: [
+ ...(yield* sandboxStore),
{
name: "OPENCODE_AUTH_CONTENT",
sensitive: true,
@@ -1244,7 +1273,7 @@ it.layer(layerUpdate)("OpenCodeDriver updates", (it) => {
const instance = yield* OpenCodeDriver.create({
instanceId: ProviderInstanceId.make("opencode-update"),
displayName: undefined,
- environment: [],
+ environment: yield* sandboxStore,
enabled: true,
config: { ...OpenCodeDriver.defaultConfig(), binaryPath },
}).pipe(Effect.provideService(HttpClient.HttpClient, noHttp));
diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts
index 9b39fed2546b5..e1807a90922c1 100644
--- a/packages/provider-opencode/src/server/driver.ts
+++ b/packages/provider-opencode/src/server/driver.ts
@@ -33,7 +33,11 @@ import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAda
import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration";
import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
import { openCodeUsageReader, type OpenCodeUsageReaderEnv } from "./usage.ts";
-import { readOpenCodeGoUsageLimits } from "./usageLimits.ts";
+import {
+ mergeUsageLimits,
+ readOpenCodeGoUsageLimits,
+ readOpenCodeIntegrationUsageLimits,
+} from "./usageLimits.ts";
import { loadOpenCode2Catalog, loadOpenCode2DirectoryCatalog } from "./openCode2Catalog.ts";
import {
checkOpenCodeProviderStatus,
@@ -58,6 +62,7 @@ import {
} from "./versionProbe.ts";
import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts";
import * as OpenCode2Client from "./v2/OpenCode2Client.ts";
+import { readProviderEndpoints } from "./v2/providerEndpoints.ts";
import * as OpenCode2Server from "./v2/OpenCode2Server.ts";
import { replaceOutdatedService } from "./v2/OpenCode2Service.ts";
import {
@@ -474,7 +479,7 @@ export const OpenCodeDriver: ProviderDriver<
Effect.provideService(Digest.Digest, digest),
);
let unsupportedUsageVersion: string | undefined;
- const checkUsageLimits = Effect.gen(function* () {
+ const checkConsoleUsageLimits = Effect.gen(function* () {
if (!effectiveConfig.enabled) return yield* legacyUsageLimits;
const runtime = yield* runtimeProbe.get;
if (runtime.generation === "v1" || runtime.version === unsupportedUsageVersion)
@@ -497,6 +502,36 @@ export const OpenCodeDriver: ProviderDriver<
})),
),
);
+ // A custom provider meters its own plan, so its quota has to be read from
+ // that provider rather than from the OpenCode Console budget. The endpoint
+ // OpenCode resolves for a provider says whose key a credential is, so it
+ // is read only for a provider that has a credential to check.
+ const checkIntegrationUsageLimits = readOpenCodeIntegrationUsageLimits({
+ enabled: effectiveConfig.enabled,
+ serverUrl: effectiveConfig.serverUrl,
+ environment: processEnv,
+ resolveEndpoint: (integrationId) =>
+ openCode2Server
+ .withConnection((connection) =>
+ readProviderEndpoints(connection, host.paths.cwd).pipe(
+ Effect.map((endpoints) => endpoints.get(integrationId)),
+ ),
+ )
+ .pipe(Effect.catchCause(() => Effect.undefined)),
+ }).pipe(
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
+ Effect.provideService(Path.Path, pathService),
+ Effect.provideService(HttpClient.HttpClient, httpClient),
+ Effect.provideService(Digest.Digest, digest),
+ );
+ // One probe for the provider's limits, so the recovery that retries a
+ // failed read retries the Console budget and the integration quotas alike.
+ const checkUsageLimits = Effect.zipWith(
+ checkConsoleUsageLimits,
+ checkIntegrationUsageLimits,
+ (usageLimits, integration) => mergeUsageLimits(usageLimits, integration),
+ { concurrent: true },
+ );
const checkProvider = Effect.all(
{
// A configured loopback server reports its running version. Probe the local
diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts
index d211563738ad5..537cffed77ed8 100644
--- a/packages/provider-opencode/src/server/usage.test.ts
+++ b/packages/provider-opencode/src/server/usage.test.ts
@@ -415,6 +415,44 @@ describe("readOpenCodeUsage", () => {
});
import * as NodeSqlite from "node:sqlite";
+describe("readOpenCodeUsage upstream provider", () => {
+ it.effect("keeps the integration that served the model", () =>
+ Effect.gen(function* () {
+ const fileSystem = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-upstream-test-" });
+ const db = yield* openDatabase(path.join(dir, "opencode.db"));
+ yield* db.unsafe(
+ "CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER)",
+ );
+ const sinceMs = 1780000000000;
+ const insert = (id: string, providerID: string) =>
+ db.unsafe("INSERT INTO message (id, session_id, data, time_created) VALUES (?, ?, ?, ?)", [
+ id,
+ "session-1",
+ JSON.stringify({
+ role: "assistant",
+ model: { id: "MiniMax-M3.1-Flash", providerID },
+ time: { created: sinceMs },
+ tokens: { input: 10, output: 5 },
+ }),
+ sinceMs,
+ ]);
+ yield* insert("msg-plan", "minimax-coding-plan");
+ yield* insert("msg-go", "opencode-go");
+
+ const result = yield* readOpenCodeUsage(dir, sinceMs);
+ const records = result.files.flatMap((file) => file.records);
+ assert.deepStrictEqual(records.map((record) => record.upstreamProvider).sort(), [
+ "minimax-coding-plan",
+ "opencode-go",
+ ]);
+ // The harness stays the provider; only the upstream integration is added.
+ assert.isTrue(records.every((record) => record.provider === "opencode"));
+ }).pipe(Effect.provide(NodeServices.layer)),
+ );
+});
+
describe("openCodeUsageReader", () => {
it.effect("scans canonical instance roots once, including XDG and home defaults", () =>
Effect.gen(function* () {
diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts
index 4ae7b6df358e9..e86dec8fff7ae 100644
--- a/packages/provider-opencode/src/server/usage.ts
+++ b/packages/provider-opencode/src/server/usage.ts
@@ -54,6 +54,10 @@ function parseOpenCodeMessage(
const modelReference = object(message.model);
const model = text(modelReference.id) || text(modelReference.modelID) || text(message.modelID);
const providerId = text(modelReference.providerID) || text(message.providerID);
+ // OpenCode fronts several upstreams (OpenCode Go, Zen, MiniMax, ...) behind
+ // one harness, and the integration id is the only thing that says which one
+ // served the model. Keep it so usage is not attributed to the wrong account.
+ const upstreamProvider = providerId || undefined;
const timestampMs = object(message.time).created ?? fallback.timestampMs;
if (!model || typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
const reasoningTokens = tokens(usage.reasoning);
@@ -71,6 +75,7 @@ function parseOpenCodeMessage(
const free = OPENCODE_GATEWAYS.has(providerId) && model.endsWith("-free");
return {
provider: "opencode",
+ ...(upstreamProvider ? { upstreamProvider } : {}),
timestampMs,
model,
...(prefix !== undefined && !model.includes("/") ? { rateModel: `${prefix}/${model}` } : {}),
diff --git a/packages/provider-opencode/src/server/usageLimits.test.ts b/packages/provider-opencode/src/server/usageLimits.test.ts
new file mode 100644
index 0000000000000..74d1264cd8262
--- /dev/null
+++ b/packages/provider-opencode/src/server/usageLimits.test.ts
@@ -0,0 +1,305 @@
+import { assert, describe, it } from "@effect/vitest";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import * as FileSystem from "effect/FileSystem";
+import { HttpClient, HttpClientResponse } from "effect/http";
+import * as Layer from "effect/Layer";
+import * as Path from "effect/Path";
+import * as SqlClient from "effect/sql/SqlClient";
+import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
+
+import { mergeUsageLimits, readOpenCodeIntegrationUsageLimits } from "./usageLimits.ts";
+
+const openDatabase = (filename: string) =>
+ Layer.build(NodeSqliteClient.layer({ filename })).pipe(
+ Effect.map(Context.get(SqlClient.SqlClient)),
+ );
+
+const MINIMAX_ENDPOINT = "https://api.minimax.io/anthropic/v1";
+
+const MINIMAX_RESPONSE = {
+ base_resp: { status_code: 0, status_msg: "success" },
+ model_remains: [
+ {
+ model_name: "general",
+ current_interval_remaining_percent: 85,
+ current_interval_status: 1,
+ end_time: 1794328907302,
+ current_weekly_remaining_percent: 91,
+ current_weekly_status: 1,
+ weekly_end_time: 1794905017270,
+ },
+ {
+ model_name: "video",
+ current_interval_remaining_percent: 0,
+ current_interval_status: 2,
+ current_weekly_remaining_percent: 0,
+ current_weekly_status: 2,
+ },
+ ],
+};
+
+/** A credential store with one active key for the given integration. */
+const storeWithCredential = (
+ integrationId: string,
+ key: string,
+ metadata?: Record,
+) =>
+ Effect.gen(function* () {
+ const fileSystem = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "opencode-limits-test-" });
+ const dataHome = path.join(dir, ".local", "share");
+ yield* fileSystem.makeDirectory(path.join(dataHome, "opencode"), { recursive: true });
+ const db = yield* openDatabase(path.join(dataHome, "opencode", "opencode.db"));
+ yield* db.unsafe(
+ "CREATE TABLE credential (id TEXT, integration_id TEXT, value TEXT, active INTEGER, time_created INTEGER)",
+ );
+ yield* db.unsafe(
+ "INSERT INTO credential (id, integration_id, value, active, time_created) VALUES (?, ?, ?, 1, 0)",
+ [
+ "cred-1",
+ integrationId,
+ JSON.stringify({ type: "key", key, ...(metadata ? { metadata } : {}) }),
+ ],
+ );
+ return { dir, dataHome };
+ });
+
+const readLimits = (
+ dataHome: string,
+ dir: string,
+ client: HttpClient.HttpClient,
+ endpoint: string | undefined,
+) =>
+ readOpenCodeIntegrationUsageLimits({
+ enabled: true,
+ serverUrl: "",
+ environment: {
+ HOME: dir,
+ XDG_DATA_HOME: dataHome,
+ },
+ resolveEndpoint: () => Effect.succeed(endpoint),
+ }).pipe(Effect.provideService(HttpClient.HttpClient, client));
+
+describe("readOpenCodeIntegrationUsageLimits", () => {
+ it.effect("reads an integration's own quota instead of the OpenCode Console budget", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-cp-plan-key");
+
+ const client = HttpClient.make((request) =>
+ Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(MINIMAX_RESPONSE))),
+ );
+ const result = yield* readLimits(dataHome, dir, client, MINIMAX_ENDPOINT);
+
+ assert.strictEqual(result.windows.length, 2);
+ const [interval, weekly] = result.windows;
+ assert.strictEqual(interval?.id, "minimax-coding-plan_interval");
+ assert.strictEqual(interval?.label, "MiniMax Token Plan · Interval");
+ assert.strictEqual(interval?.usedPercent, 15);
+ assert.strictEqual(weekly?.id, "minimax-coding-plan_weekly");
+ assert.strictEqual(weekly?.usedPercent, 9);
+ assert.isDefined(interval?.resetsAt);
+ assert.isDefined(result.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("reports only the bucket that meters the plan", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-cp-plan-key");
+
+ const client = HttpClient.make((request) =>
+ Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(MINIMAX_RESPONSE))),
+ );
+ const result = yield* readLimits(dataHome, dir, client, MINIMAX_ENDPOINT);
+
+ // The video bucket is exhausted; reporting it would exhaust the plan too.
+ assert.isTrue(result.windows.every((window) => window.usedPercent < 100));
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("fingerprints a plan by its own credential, not by the account", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-cp-plan-key");
+ const client = HttpClient.make((request) =>
+ Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(MINIMAX_RESPONSE))),
+ );
+ const endpoints = MINIMAX_ENDPOINT;
+ const alone = yield* readLimits(dataHome, dir, client, endpoints);
+ const plan = alone.windows.find((window) => window.id === "minimax-coding-plan_weekly");
+ assert.isDefined(plan?.credentialFingerprint);
+
+ // The same plan next to another credential keeps its own fingerprint.
+ const withBoth = yield* Effect.gen(function* () {
+ const path = yield* Path.Path;
+ const db = yield* openDatabase(path.join(dataHome, "opencode", "opencode.db"));
+ yield* db.unsafe(
+ "INSERT INTO credential (id, integration_id, value, active, time_created) VALUES (?, ?, ?, 1, 0)",
+ ["cred-2", "minimax", JSON.stringify({ type: "key", key: "sk-cp-other-key" })],
+ );
+ return yield* readLimits(dataHome, dir, client, endpoints);
+ });
+ const samePlan = withBoth.windows.find(
+ (window) => window.id === "minimax-coding-plan_weekly",
+ );
+ assert.strictEqual(samePlan?.credentialFingerprint, plan?.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("never sends a key that MiniMax did not issue", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-third-party");
+
+ const client = HttpClient.make((request) =>
+ Effect.die(`The key left the machine: ${request.url}`),
+ );
+ const result = yield* readLimits(dataHome, dir, client, MINIMAX_ENDPOINT);
+
+ assert.deepStrictEqual(result.windows, []);
+ assert.isUndefined(result.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("never sends a key that the provider points at another endpoint", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-cp-plan-key");
+
+ const client = HttpClient.make((request) =>
+ Effect.die(`The key left the machine: ${request.url}`),
+ );
+ const result = yield* readLimits(dataHome, dir, client, "https://gateway.example/v1");
+
+ assert.deepStrictEqual(result.windows, []);
+ assert.isUndefined(result.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("never sends a key whose owner is unknown", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential("minimax-coding-plan", "sk-cp-plan-key");
+
+ const client = HttpClient.make((request) =>
+ Effect.die(`The key left the machine: ${request.url}`),
+ );
+ const result = yield* readLimits(dataHome, dir, client, undefined);
+
+ assert.deepStrictEqual(result.windows, []);
+ assert.isUndefined(result.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+
+ it.effect("never sends a key that names another endpoint itself", () =>
+ Effect.gen(function* () {
+ const { dir, dataHome } = yield* storeWithCredential(
+ "minimax-coding-plan",
+ "sk-cp-plan-key",
+ {
+ baseURL: "https://gateway.example/v1",
+ },
+ );
+
+ const client = HttpClient.make((request) =>
+ Effect.die(`The key left the machine: ${request.url}`),
+ );
+ const result = yield* readLimits(dataHome, dir, client, MINIMAX_ENDPOINT);
+
+ assert.deepStrictEqual(result.windows, []);
+ assert.isUndefined(result.credentialFingerprint);
+ }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
+ );
+});
+
+describe("mergeUsageLimits", () => {
+ it("keeps the integration windows and drops the unavailable marker", () => {
+ const merged = mergeUsageLimits(
+ {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [],
+ credentialFingerprint: "go-account",
+ unavailable: { reason: "unsupported" },
+ },
+ {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [
+ {
+ id: "minimax-coding-plan_weekly",
+ kind: "weekly",
+ label: "MiniMax Token Plan · Weekly",
+ usedPercent: 9,
+ },
+ ],
+ },
+ );
+ assert.strictEqual(merged.windows.length, 1);
+ assert.strictEqual(merged.credentialFingerprint, "go-account");
+ assert.isUndefined(merged.unavailable);
+ });
+
+ it("keeps the Console budget untouched when no integration reports a quota", () => {
+ const console = {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [],
+ unavailable: { reason: "unsupported" as const },
+ };
+ assert.deepStrictEqual(
+ mergeUsageLimits(console, { checkedAt: console.checkedAt, windows: [] }),
+ console,
+ );
+ });
+
+ it("tells two accounts apart when only the integration key differs", () => {
+ const console = {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [],
+ credentialFingerprint: "go-account",
+ };
+ const merged = mergeUsageLimits(console, {
+ checkedAt: console.checkedAt,
+ windows: [
+ {
+ id: "minimax-coding-plan_weekly",
+ kind: "weekly",
+ label: "MiniMax Token Plan · Weekly",
+ usedPercent: 9,
+ },
+ ],
+ credentialFingerprint: "minimax-account",
+ });
+ assert.strictEqual(merged.credentialFingerprint, "go-account.minimax-account");
+ });
+
+ it("names the credential that meters each window", () => {
+ const merged = mergeUsageLimits(
+ {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [
+ {
+ id: "go_weekly",
+ kind: "weekly",
+ label: "Go · Weekly",
+ usedPercent: 20,
+ },
+ ],
+ credentialFingerprint: "go-account",
+ },
+ {
+ checkedAt: "2026-11-10T00:00:00.000Z",
+ windows: [
+ {
+ id: "minimax-coding-plan_weekly",
+ kind: "weekly",
+ label: "MiniMax Token Plan · Weekly",
+ usedPercent: 9,
+ credentialFingerprint: "plan-account",
+ },
+ ],
+ credentialFingerprint: "minimax-account",
+ },
+ );
+ assert.strictEqual(merged.windows[0]?.credentialFingerprint, "go-account");
+ // The integration window keeps the credential that meters it.
+ assert.strictEqual(merged.windows[1]?.credentialFingerprint, "plan-account");
+ });
+});
diff --git a/packages/provider-opencode/src/server/usageLimits.ts b/packages/provider-opencode/src/server/usageLimits.ts
index 03488e515a79f..c81dbb1d2f6ed 100644
--- a/packages/provider-opencode/src/server/usageLimits.ts
+++ b/packages/provider-opencode/src/server/usageLimits.ts
@@ -1,6 +1,6 @@
import * as NodeSqlite from "node:sqlite";
-import type { ServerProviderUsageWindow } from "@t3tools/contracts";
+import type { ServerProviderUsageLimits, ServerProviderUsageWindow } from "@t3tools/contracts";
import * as Digest from "effect/crypto/Digest";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
@@ -29,8 +29,14 @@ const StoredKey = Schema.Struct({
type: Schema.Literal("key"),
key: Schema.String,
metadata: Schema.optionalKey(
- Schema.NullOr(Schema.Struct({ server: Schema.optionalKey(Schema.Unknown) })),
+ Schema.NullOr(
+ Schema.Struct({
+ server: Schema.optionalKey(Schema.Unknown),
+ baseURL: Schema.optionalKey(Schema.Unknown),
+ }),
+ ),
),
+ configuration: Schema.optionalKey(Schema.Struct({ baseURL: Schema.optionalKey(Schema.Unknown) })),
});
const decodeStoredKey = Schema.decodeOption(Schema.fromJsonString(StoredKey));
const UsageWindow = Schema.Struct({
@@ -41,6 +47,91 @@ const UsageResponse = Schema.Struct({
usage: Schema.Struct({ rolling: UsageWindow, weekly: UsageWindow, monthly: UsageWindow }),
});
+/**
+ * Upstream integrations that publish their own quota. OpenCode fronts several
+ * providers behind one harness, and each of them meters its own plan, so the
+ * quota has to be read from the integration that serves the model rather than
+ * from the OpenCode Console budget.
+ */
+const INTEGRATION_QUOTA_ENDPOINTS: Readonly<
+ Record
+> = {
+ "minimax-coding-plan": {
+ endpoint: "https://api.minimax.io/v1/token_plan/remains",
+ label: "MiniMax Token Plan",
+ },
+ minimax: {
+ endpoint: "https://api.minimax.io/v1/token_plan/remains",
+ label: "MiniMax",
+ },
+};
+
+/**
+ * MiniMax issues its token-plan keys with this prefix. A pay-as-you-go key
+ * meters no plan, and a key that belongs to another endpoint must never leave
+ * the machine, so only a plan key is sent to the quota API.
+ */
+const MINI_MAX_PLAN_KEY_PREFIX = "sk-cp-";
+
+/** The hosts that serve MiniMax's own API. */
+const MINI_MAX_HOSTS = new Set(["api.minimax.io", "api.minimaxi.com"]);
+
+// MiniMax answers with one entry per quota and two metered windows.
+const MiniMaxQuotaResponse = Schema.Struct({
+ base_resp: Schema.optionalKey(
+ Schema.Struct({
+ status_code: Schema.optionalKey(Schema.Finite),
+ status_msg: Schema.optionalKey(Schema.String),
+ }),
+ ),
+ model_remains: Schema.optionalKey(Schema.Array(Schema.Unknown)),
+});
+
+interface MiniMaxQuotaEntry {
+ readonly model_name?: string;
+ readonly current_interval_remaining_percent?: number;
+ readonly current_interval_status?: number;
+ readonly end_time?: number;
+ readonly start_time?: number;
+ readonly weekly_end_time?: number;
+ readonly weekly_start_time?: number;
+ readonly current_weekly_remaining_percent?: number;
+ readonly current_weekly_status?: number;
+}
+
+const miniMaxQuotaEntry = Schema.decodeUnknownOption(
+ Schema.Struct({
+ model_name: Schema.optionalKey(Schema.String),
+ current_interval_remaining_percent: Schema.optionalKey(Schema.Finite),
+ current_interval_status: Schema.optionalKey(Schema.Finite),
+ end_time: Schema.optionalKey(Schema.Finite),
+ start_time: Schema.optionalKey(Schema.Finite),
+ weekly_end_time: Schema.optionalKey(Schema.Finite),
+ weekly_start_time: Schema.optionalKey(Schema.Finite),
+ current_weekly_remaining_percent: Schema.optionalKey(Schema.Finite),
+ current_weekly_status: Schema.optionalKey(Schema.Finite),
+ }),
+);
+
+/**
+ * The bucket that meters the plan every chat model draws from. MiniMax reports
+ * one bucket per quota, and the others (a video allowance, for example) meter a
+ * quota no model in T3 Code spends.
+ */
+const MINI_MAX_PLAN_BUCKET = "general";
+
+/**
+ * The plan's own bucket. Reporting a second bucket under the same window ids
+ * would pool two different quotas into one percentage, so only the bucket that
+ * meters the plan is reported.
+ */
+function miniMaxPlanBucket(entries: readonly MiniMaxQuotaEntry[]): MiniMaxQuotaEntry | undefined {
+ const plan = entries.find((entry) => entry.model_name === MINI_MAX_PLAN_BUCKET);
+ if (plan) return plan;
+ // A payload with a single bucket leaves no room for ambiguity.
+ return entries.length === 1 ? entries[0] : undefined;
+}
+
// Select before decoding so an OAuth or self-hosted connection cannot fall
// through to another stored account. NULL-active rows predate the selection flag.
class OpenCodeCredentialReadError extends Schema.TaggedError()(
@@ -52,59 +143,370 @@ class OpenCodeCredentialReadError extends Schema.TaggedError
+interface StoredCredential {
+ readonly key: string;
+ /** The OpenCode deployment the credential belongs to, when the store names one. */
+ readonly server: string | undefined;
+ /** The endpoint the credential reaches, when the store names one. */
+ readonly baseURL: string | undefined;
+}
+
+/**
+ * Every active credential in the local OpenCode store, newest first per
+ * integration. One read, so every caller sees the same store.
+ */
+const readStoredCredentials = (databasePath: string) =>
Effect.try({
try: () => {
const database = new NodeSqlite.DatabaseSync(databasePath, { readOnly: true });
try {
database.exec("PRAGMA busy_timeout = 100");
- const row = database
+ const rows = database
.prepare(
- `SELECT value FROM credential
- WHERE integration_id = ? AND active IS NOT 0
- ORDER BY active DESC, time_created DESC, id DESC LIMIT 1`,
+ `SELECT integration_id, value FROM credential
+ WHERE active IS NOT 0 ORDER BY active DESC, time_created DESC, id DESC`,
)
- .get(integrationId);
- const stored =
- typeof row?.value === "string"
- ? Option.getOrUndefined(decodeStoredKey(row.value))
- : undefined;
- const server = stored?.metadata?.server;
- if (typeof server === "string") {
- const url = new URL(server);
- if (`${url.origin}${url.pathname.replace(/\/+$/, "")}` !== "https://opencode.ai/console")
- return undefined;
+ .all() as Array<{ readonly integration_id: unknown; readonly value: unknown }>;
+ const credentials = new Map();
+ for (const row of rows) {
+ const integrationId = typeof row.integration_id === "string" ? row.integration_id : "";
+ if (integrationId === "" || credentials.has(integrationId)) continue;
+ const stored =
+ typeof row.value === "string"
+ ? Option.getOrUndefined(decodeStoredKey(row.value))
+ : undefined;
+ const key = stored?.key.trim();
+ if (!key) continue;
+ const server = stored?.metadata?.server;
+ const configuredBaseURL =
+ typeof stored?.metadata?.baseURL === "string"
+ ? stored.metadata.baseURL
+ : typeof stored?.configuration?.baseURL === "string"
+ ? stored.configuration.baseURL
+ : undefined;
+ credentials.set(integrationId, {
+ key,
+ server: typeof server === "string" ? server : undefined,
+ baseURL: configuredBaseURL,
+ });
}
- return stored?.key.trim();
+ return credentials;
} finally {
database.close();
}
},
catch: (cause) => new OpenCodeCredentialReadError({ databasePath, cause }),
- }).pipe(Effect.orElseSucceed(() => undefined));
+ }).pipe(Effect.orElseSucceed(() => new Map()));
-/** External OpenCode servers own their credentials; never read the host's account for them. */
-export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(function* (input: {
+/** A Zen key belongs to the deployment that stored it, so only the Console's own is used. */
+function isOfficialConsole(server: string | undefined): boolean {
+ if (server === undefined) return true;
+ try {
+ const url = new URL(server);
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}` === "https://opencode.ai/console";
+ } catch {
+ return false;
+ }
+}
+
+/** A credential that names another server belongs to that server, not to MiniMax. */
+function servesMiniMax(server: string | undefined): boolean {
+ if (server === undefined) return true;
+ try {
+ const url = new URL(server);
+ return MINI_MAX_HOSTS.has(url.host);
+ } catch {
+ return false;
+ }
+}
+
+/** The endpoint MiniMax serves its own API from. */
+function isMiniMaxEndpoint(baseURL: string | undefined): boolean {
+ if (baseURL === undefined) return false;
+ try {
+ return MINI_MAX_HOSTS.has(new URL(baseURL).host);
+ } catch {
+ return false;
+ }
+}
+
+/** The local OpenCode store: where its data home, auth file and credential database live. */
+const openCodeStorePaths = Effect.fn("OpenCodeStore.paths")(function* (
+ environment: NodeJS.ProcessEnv,
+) {
+ const path = yield* Path.Path;
+ const dataHome =
+ environment.XDG_DATA_HOME ||
+ path.join(
+ environment.HOME || environment.USERPROFILE || (yield* HostProcess.HomeDirectory),
+ ".local",
+ "share",
+ );
+ const databaseFile = environment.OPENCODE_DB ?? "opencode.db";
+ return {
+ dataHome,
+ authPath: path.join(dataHome, "opencode", "auth.json"),
+ databasePath: path.resolve(dataHome, "opencode", databaseFile),
+ };
+});
+
+const miniMaxWindow = (
+ integrationId: string,
+ label: string,
+ id: string,
+ kind: "session" | "weekly",
+ remainingPercent: number | undefined,
+ statusCode: number | undefined,
+ resetsAtMs: number | undefined,
+ startsAtMs: number | undefined,
+): ServerProviderUsageWindow | null => {
+ if (remainingPercent === undefined) return null;
+ // MiniMax reports 1 normal, 2 exhausted and 3 unlimited.
+ if (statusCode === 3) return null;
+ const usedPercent = clampPercent(100 - remainingPercent);
+ const resetsAt =
+ resetsAtMs !== undefined && Number.isFinite(resetsAtMs)
+ ? Option.getOrUndefined(DateTime.make(resetsAtMs))
+ : undefined;
+ // The window length is what lets the client judge pace, so take it from the
+ // window's own bounds.
+ const windowDurationMins =
+ startsAtMs !== undefined &&
+ resetsAtMs !== undefined &&
+ Number.isFinite(startsAtMs) &&
+ Number.isFinite(resetsAtMs) &&
+ resetsAtMs > startsAtMs
+ ? Math.round((resetsAtMs - startsAtMs) / 60_000)
+ : undefined;
+ return {
+ id: `${integrationId}_${id}`,
+ kind,
+ label: `${label} · ${kind === "session" ? "Interval" : "Weekly"}`,
+ usedPercent,
+ ...(windowDurationMins === undefined ? {} : { windowDurationMins }),
+ ...(resetsAt ? { resetsAt: DateTime.formatIso(resetsAt) } : {}),
+ };
+};
+
+export interface OpenCodeUsageLimitsInput {
readonly enabled: boolean;
readonly serverUrl: string;
readonly environment: NodeJS.ProcessEnv;
-}) {
+ /**
+ * The endpoint OpenCode resolves for a provider. A provider without one is
+ * not probed: the key's owner would be unknown.
+ */
+ readonly resolveEndpoint?:
+ | ((integrationId: string) => Effect.Effect)
+ | undefined;
+}
+
+export interface OpenCodeIntegrationUsageLimits {
+ readonly checkedAt: string;
+ readonly windows: readonly ServerProviderUsageWindow[];
+ /** The identity of the credentials that meter the reported windows. */
+ readonly credentialFingerprint?: string | undefined;
+}
+
+/**
+ * Quota for the upstream integrations the user actually runs. OpenCode Go is
+ * the Console budget; a custom provider meters its own plan, so its windows
+ * come from that provider's API.
+ */
+export const readOpenCodeIntegrationUsageLimits = Effect.fn("readOpenCodeIntegrationUsageLimits")(
+ function* (input: OpenCodeUsageLimitsInput) {
+ const checkedAt = DateTime.formatIso(yield* DateTime.now);
+ const empty = {
+ checkedAt,
+ windows: [] as readonly ServerProviderUsageWindow[],
+ credentialFingerprint: undefined as string | undefined,
+ };
+ if (!input.enabled || input.serverUrl.trim()) return empty;
+
+ return yield* Effect.gen(function* () {
+ const env = input.environment;
+ const { databasePath } = yield* openCodeStorePaths(env);
+ const storedCredentials = yield* readStoredCredentials(databasePath);
+ // Only a credential that MiniMax itself issued may reach MiniMax's API.
+ const candidates: Array = [];
+ for (const integrationId of Object.keys(INTEGRATION_QUOTA_ENDPOINTS)) {
+ const credential = storedCredentials.get(integrationId);
+ if (credential === undefined) continue;
+ if (!credential.key.startsWith(MINI_MAX_PLAN_KEY_PREFIX)) continue;
+ if (!servesMiniMax(credential.server)) continue;
+ // The provider must resolve to MiniMax's own endpoint, and the
+ // credential must not point somewhere else.
+ const endpoint =
+ input.resolveEndpoint === undefined
+ ? undefined
+ : yield* input.resolveEndpoint(integrationId);
+ if (endpoint === undefined || !isMiniMaxEndpoint(endpoint)) continue;
+ if (credential.baseURL !== undefined && !isMiniMaxEndpoint(credential.baseURL)) continue;
+ candidates.push([integrationId, credential]);
+ }
+ if (candidates.length === 0) return empty;
+
+ const digest = yield* Digest.Digest;
+ const fingerprint = (value: string) =>
+ digest
+ .digest(Digest.sha256, new TextEncoder().encode(value))
+ .pipe(Effect.map(Hex.encode), Effect.orDie);
+ // The account names every credential that meters it, so two environments
+ // that share one key but differ in another stay apart.
+ const credentialFingerprint = yield* fingerprint(
+ candidates
+ .map(
+ ([integrationId, credential]) =>
+ `opencode-integrations\0${integrationId}\0${credential.key}`,
+ )
+ .sort()
+ .join("\0"),
+ );
+ // Each window names the credential that meters it, so the same plan on
+ // two environments is one quota.
+ const windowFingerprints = new Map();
+ for (const [integrationId, credential] of candidates) {
+ windowFingerprints.set(
+ integrationId,
+ yield* fingerprint(`opencode-integration\0${integrationId}\0${credential.key}`),
+ );
+ }
+
+ const client = yield* HttpClient.HttpClient;
+ const windows: ServerProviderUsageWindow[] = [];
+ for (const [integrationId, credential] of candidates) {
+ const quota = INTEGRATION_QUOTA_ENDPOINTS[integrationId];
+ if (quota === undefined) continue;
+ const response = yield* client
+ .execute(
+ HttpClientRequest.get(quota.endpoint).pipe(
+ HttpClientRequest.bearerToken(credential.key),
+ ),
+ )
+ .pipe(Effect.timeout("5 seconds"), Effect.option);
+ if (Option.isNone(response)) continue;
+ const result = response.value;
+ if (result.status !== 200) continue;
+ // The body read needs its own bound: a server that sends headers and
+ // then stalls would hold the refresh permit forever.
+ const body = yield* HttpClientResponse.schemaBodyJson(MiniMaxQuotaResponse)(result).pipe(
+ Effect.timeout("5 seconds"),
+ Effect.option,
+ );
+ if (Option.isNone(body)) continue;
+ const payload = body.value;
+ if ((payload.base_resp?.status_code ?? 0) !== 0) continue;
+ const entries = (payload.model_remains ?? []).flatMap((raw) => {
+ const entry = Option.getOrUndefined(miniMaxQuotaEntry(raw));
+ return entry ? [entry] : [];
+ });
+ const plan = miniMaxPlanBucket(entries);
+ if (plan === undefined) continue;
+ const interval = miniMaxWindow(
+ integrationId,
+ quota.label,
+ "interval",
+ "session",
+ plan.current_interval_remaining_percent,
+ plan.current_interval_status,
+ plan.end_time,
+ plan.start_time,
+ );
+ const weekly = miniMaxWindow(
+ integrationId,
+ quota.label,
+ "weekly",
+ "weekly",
+ plan.current_weekly_remaining_percent,
+ plan.current_weekly_status,
+ plan.weekly_end_time,
+ plan.weekly_start_time,
+ );
+ const credentialFingerprint = windowFingerprints.get(integrationId);
+ if (interval !== null)
+ windows.push(
+ credentialFingerprint === undefined ? interval : { ...interval, credentialFingerprint },
+ );
+ if (weekly !== null)
+ windows.push(
+ credentialFingerprint === undefined ? weekly : { ...weekly, credentialFingerprint },
+ );
+ }
+ return { checkedAt, windows, credentialFingerprint };
+ }).pipe(
+ Effect.orElseSucceed(() => ({
+ checkedAt,
+ windows: [] as readonly ServerProviderUsageWindow[],
+ credentialFingerprint: undefined as string | undefined,
+ })),
+ );
+ },
+);
+
+/**
+ * The account identity covers every credential that meters it, so two
+ * environments that share a Console key but not an integration key stay
+ * separate accounts.
+ */
+function mergeFingerprints(
+ console: string | undefined,
+ integration: string | undefined,
+): string | undefined {
+ if (console === undefined) return integration;
+ if (integration === undefined) return console;
+ return `${console}.${integration}`;
+}
+
+/**
+ * Combine the Console budget with the quotas of the integrations in use.
+ *
+ * The Console budget can be unavailable while a custom provider still meters
+ * a plan, so the unavailable marker is dropped once there are windows to
+ * report.
+ */
+export function mergeUsageLimits(
+ console: ServerProviderUsageLimits,
+ integration: OpenCodeIntegrationUsageLimits,
+): ServerProviderUsageLimits {
+ // Without a quota of its own the account is what the Console says it is.
+ if (integration.windows.length === 0) return console;
+ // The integration's quota replaces the unavailable marker, but the Console
+ // metadata stays: its fingerprint is what tells one account apart from
+ // another when several environments report the same one.
+ const { unavailable: _unavailable, ...consoleMetadata } = console;
+ const credentialFingerprint = mergeFingerprints(
+ consoleMetadata.credentialFingerprint,
+ integration.credentialFingerprint,
+ );
+ // The Console windows name the Console credential; an integration window
+ // already names the credential that meters it.
+ const consoleWindows =
+ consoleMetadata.credentialFingerprint === undefined
+ ? console.windows
+ : console.windows.map((window) => ({
+ ...window,
+ credentialFingerprint: consoleMetadata.credentialFingerprint,
+ }));
+ return {
+ ...consoleMetadata,
+ ...(credentialFingerprint === undefined ? {} : { credentialFingerprint }),
+ checkedAt: console.checkedAt,
+ windows: [...consoleWindows, ...integration.windows],
+ };
+}
+
+/** External OpenCode servers own their credentials; never read the host's account for them. */
+export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(function* (
+ input: OpenCodeUsageLimitsInput,
+) {
const checkedAt = DateTime.formatIso(yield* DateTime.now);
const unsupported = makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
if (!input.enabled || input.serverUrl.trim()) return unsupported;
return yield* Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
const env = input.environment;
- const dataHome =
- env.XDG_DATA_HOME ||
- path.join(
- env.HOME || env.USERPROFILE || (yield* HostProcess.HomeDirectory),
- ".local",
- "share",
- );
- const authPath = path.join(dataHome, "opencode", "auth.json");
+ const { authPath, databasePath } = yield* openCodeStorePaths(env);
const contents =
env.OPENCODE_AUTH_CONTENT ||
(yield* fs.readFileString(authPath).pipe(
@@ -119,17 +521,18 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
const goKey = (Option.isSome(apiAuth) ? apiAuth.value.key : env.OPENCODE_API_KEY)?.trim();
// Console supplies Go credentials in OpenCode 2. Inline auth explicitly
// overrides local credentials, including the Console connection.
- const databaseFile = env.OPENCODE_DB ?? "opencode.db";
- const databasePath = path.resolve(dataHome, "opencode", databaseFile);
- const storedKeys =
- env.OPENCODE_AUTH_CONTENT || databaseFile === ":memory:"
- ? []
- : [
- yield* readStoredApiKey(databasePath, "opencode"),
- // OpenCode 2 imports auth.json once, so a replaced Go key only exists here.
- yield* readStoredApiKey(databasePath, "opencode-go"),
- ];
- const keys = [...new Set([...storedKeys, goKey].filter((key): key is string => Boolean(key)))];
+ const storedCredentials =
+ env.OPENCODE_AUTH_CONTENT || env.OPENCODE_DB === ":memory:"
+ ? new Map()
+ : yield* readStoredCredentials(databasePath);
+ const consoleKey = ["opencode", "opencode-go"]
+ .map((integrationId) => storedCredentials.get(integrationId))
+ .filter(
+ (credential): credential is StoredCredential =>
+ credential !== undefined && isOfficialConsole(credential.server),
+ )
+ .map((credential) => credential.key);
+ const keys = [...new Set([...consoleKey, goKey].filter((key): key is string => Boolean(key)))];
let apiKey = keys[0];
if (!apiKey) return unsupported;
@@ -182,7 +585,12 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
.digest(Digest.sha256, new TextEncoder().encode(`opencode-go\0${apiKey}`))
.pipe(Effect.map(Hex.encode), Effect.orDie);
return {
- ...makeUsageLimits({ checkedAt, windows }),
+ ...makeUsageLimits({
+ checkedAt,
+ // The window names the credential that meters it, so a pool counts one
+ // Go account once even when two environments report it.
+ windows: windows.map((window) => ({ ...window, credentialFingerprint })),
+ }),
credentialFingerprint,
};
}).pipe(
diff --git a/packages/provider-opencode/src/server/v2/providerEndpoints.test.ts b/packages/provider-opencode/src/server/v2/providerEndpoints.test.ts
new file mode 100644
index 0000000000000..916cb01791084
--- /dev/null
+++ b/packages/provider-opencode/src/server/v2/providerEndpoints.test.ts
@@ -0,0 +1,162 @@
+import { assert, describe, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+
+import type * as OpenCode2Server from "./OpenCode2Server.ts";
+import { readProviderEndpoints } from "./providerEndpoints.ts";
+
+interface ProviderEntry {
+ readonly id: string;
+ readonly integrationID?: string;
+ readonly baseURL?: string;
+}
+
+interface ModelEntry {
+ readonly providerID: string;
+ readonly baseURL?: string;
+}
+
+/** The provider list OpenCode answers with, with its resolved settings. */
+const providerEntry = ({ id, integrationID, baseURL }: ProviderEntry) => ({
+ id,
+ ...(integrationID === undefined ? {} : { integrationID }),
+ ...(baseURL === undefined ? {} : { settings: { baseURL } }),
+});
+
+/** The model list OpenCode answers with, with its resolved settings. */
+const modelEntry = ({ providerID, baseURL }: ModelEntry) => ({
+ providerID,
+ ...(baseURL === undefined ? {} : { settings: { baseURL } }),
+});
+
+const connectionWith = (providers: readonly ProviderEntry[], models: readonly ModelEntry[] = []) =>
+ ({
+ client: {
+ provider: {
+ list: () => Effect.succeed({ data: providers.map(providerEntry) }),
+ },
+ model: {
+ list: () => Effect.succeed({ data: models.map(modelEntry) }),
+ },
+ },
+ }) as unknown as OpenCode2Server.OpenCode2Connection;
+
+describe("readProviderEndpoints", () => {
+ it.effect("names the endpoint of every provider and of its integration", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ connectionWith([
+ { id: "minimax-coding-plan", integrationID: "minimax-coding-plan" },
+ { id: "opencode", integrationID: "opencode", baseURL: "https://opencode.ai/console" },
+ ]),
+ "/work",
+ );
+ assert.strictEqual(endpoints.get("minimax-coding-plan"), undefined);
+ assert.strictEqual(endpoints.get("opencode"), "https://opencode.ai/console");
+ }),
+ );
+
+ it.effect("leaves an integration unknown when its providers disagree", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ connectionWith([
+ {
+ id: "minimax-coding-plan",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://api.minimax.io/anthropic/v1",
+ },
+ {
+ id: "minimax-gateway",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://gateway.example/v1",
+ },
+ ]),
+ "/work",
+ );
+ // The probe must not pick one of the two endpoints.
+ assert.strictEqual(endpoints.get("minimax-coding-plan"), undefined);
+ assert.strictEqual(endpoints.get("minimax-gateway"), "https://gateway.example/v1");
+ }),
+ );
+
+ it.effect("leaves an integration unknown when one of its providers has no endpoint", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ connectionWith([
+ {
+ id: "minimax-coding-plan",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://api.minimax.io/anthropic/v1",
+ },
+ { id: "minimax-gateway", integrationID: "minimax-coding-plan" },
+ ]),
+ "/work",
+ );
+ assert.strictEqual(endpoints.get("minimax-coding-plan"), undefined);
+ }),
+ );
+
+ it.effect("leaves a provider unknown when one of its models resolves elsewhere", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ connectionWith(
+ [
+ {
+ id: "minimax-coding-plan",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://api.minimax.io/anthropic/v1",
+ },
+ ],
+ [{ providerID: "minimax-coding-plan", baseURL: "https://gateway.example/v1" }],
+ ),
+ "/work",
+ );
+ assert.strictEqual(endpoints.get("minimax-coding-plan"), undefined);
+ }),
+ );
+
+ it.effect("keeps an endpoint when its providers and models agree", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ connectionWith(
+ [
+ {
+ id: "minimax-coding-plan",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://api.minimax.io/anthropic/v1",
+ },
+ {
+ id: "minimax-coding-plan-eu",
+ integrationID: "minimax-coding-plan",
+ baseURL: "https://api.minimax.io/anthropic/v1",
+ },
+ ],
+ [{ providerID: "minimax-coding-plan", baseURL: "https://api.minimax.io/anthropic/v1" }],
+ ),
+ "/work",
+ );
+ assert.strictEqual(
+ endpoints.get("minimax-coding-plan"),
+ "https://api.minimax.io/anthropic/v1",
+ );
+ }),
+ );
+
+ it.effect("returns no endpoints when the provider list cannot be read", () =>
+ Effect.gen(function* () {
+ const endpoints = yield* readProviderEndpoints(
+ {
+ client: {
+ provider: {
+ list: () => Effect.die("no server"),
+ },
+ model: {
+ list: () => Effect.succeed({ data: [] }),
+ },
+ },
+ } as unknown as OpenCode2Server.OpenCode2Connection,
+ "/work",
+ );
+ assert.strictEqual(endpoints.size, 0);
+ }),
+ );
+});
diff --git a/packages/provider-opencode/src/server/v2/providerEndpoints.ts b/packages/provider-opencode/src/server/v2/providerEndpoints.ts
new file mode 100644
index 0000000000000..39876a23c69a4
--- /dev/null
+++ b/packages/provider-opencode/src/server/v2/providerEndpoints.ts
@@ -0,0 +1,70 @@
+import * as Effect from "effect/Effect";
+
+import type * as OpenCode2Server from "./OpenCode2Server.ts";
+
+/**
+ * The endpoint OpenCode resolves for every provider, by provider id and by
+ * integration id. An empty map means the endpoint is unknown, so a credential
+ * is not sent anywhere.
+ */
+export const readProviderEndpoints = Effect.fn("OpenCode2Client.providerEndpoints")(function* (
+ connection: OpenCode2Server.OpenCode2Connection,
+ directory: string,
+) {
+ return yield* Effect.all(
+ [
+ connection.client.provider.list({ location: { directory } }),
+ connection.client.model.list({ location: { directory } }),
+ ],
+ { concurrency: "unbounded" },
+ ).pipe(
+ Effect.map(([providers, models]) => {
+ const endpoints = new Map();
+ // A provider whose endpoint is unknown, or whose providers disagree, leaves
+ // the integration unknown: the probe must not guess which one meters the key.
+ const unknown = new Set();
+ const record = (key: string, baseURL: string | undefined) => {
+ if (unknown.has(key)) return;
+ if (baseURL === undefined) {
+ endpoints.delete(key);
+ unknown.add(key);
+ return;
+ }
+ const known = endpoints.get(key);
+ if (known === undefined) {
+ endpoints.set(key, baseURL);
+ return;
+ }
+ if (known !== baseURL) {
+ endpoints.delete(key);
+ unknown.add(key);
+ }
+ };
+ const providerEndpoint = new Map();
+ for (const provider of providers.data) {
+ providerEndpoint.set(
+ provider.id,
+ typeof provider.settings?.baseURL === "string" ? provider.settings.baseURL : undefined,
+ );
+ }
+ // A model can resolve to another endpoint on its own, so the provider's
+ // endpoint is not the one every model goes to.
+ for (const model of models.data) {
+ const baseURL =
+ typeof model.settings?.baseURL === "string" ? model.settings.baseURL : undefined;
+ if (baseURL === undefined) continue;
+ const provider = providerEndpoint.get(model.providerID);
+ if (provider !== undefined && provider !== baseURL) {
+ providerEndpoint.set(model.providerID, undefined);
+ }
+ }
+ for (const provider of providers.data) {
+ const baseURL = providerEndpoint.get(provider.id);
+ record(provider.id, baseURL);
+ if (provider.integrationID !== undefined) record(provider.integrationID, baseURL);
+ }
+ return endpoints;
+ }),
+ Effect.catchCause(() => Effect.succeed(new Map())),
+ );
+});
diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts
index 9d2bf395a9d72..88da0c4f13b40 100644
--- a/packages/shared/src/usageLimits.test.ts
+++ b/packages/shared/src/usageLimits.test.ts
@@ -180,6 +180,32 @@ describe("new thread model fallback", () => {
["opencode", "console-custom"],
],
["opencode", ["budget_all"], "opencode/model", false, undefined, []],
+ ["opencode", ["minimax_weekly"], "minimax/MiniMax-M3.1-Flash", true, undefined, undefined],
+ [
+ "opencode",
+ ["minimax-coding-plan_weekly"],
+ "minimax/MiniMax-M3.1-Flash",
+ false,
+ undefined,
+ undefined,
+ ],
+ [
+ "opencode",
+ ["minimax-coding-plan_weekly"],
+ "minimax-coding-plan/MiniMax-M3",
+ true,
+ undefined,
+ undefined,
+ ],
+ [
+ "opencode",
+ ["minimax-coding-plan_weekly"],
+ "opencode-go/big-pickle",
+ false,
+ undefined,
+ undefined,
+ ],
+ ["opencode", ["go_weekly"], "minimax/MiniMax-M3.1-Flash", false, undefined, undefined],
] as const)(
"matches %s pools %s to %s",
(driver, ids, model, usesFallback, modelIds, providerIds) => {
@@ -1056,6 +1082,55 @@ describe("pooled account columns", () => {
row.columns.map((member) => (member.window ? member.account.key : null)),
);
+ it("counts one credential once in a pool", () => {
+ const accounts = [
+ {
+ ...account("env-a", [
+ { ...weekly, id: "go_weekly", usedPercent: 20, credentialFingerprint: "go-key" },
+ ]),
+ driver: ProviderDriverKind.make("opencode"),
+ },
+ {
+ // The same Go credential on another environment, with a plan of its own.
+ ...account("env-b", [
+ { ...weekly, id: "go_weekly", usedPercent: 20, credentialFingerprint: "go-key" },
+ ]),
+ driver: ProviderDriverKind.make("opencode"),
+ },
+ {
+ ...account("env-c", [
+ { ...weekly, id: "go_weekly", usedPercent: 80, credentialFingerprint: "other-go-key" },
+ ]),
+ driver: ProviderDriverKind.make("opencode"),
+ },
+ ];
+ const [pool] = collectLimitPools(accounts, now);
+ expect(pool?.windows[0]?.members).toHaveLength(2);
+ expect(pool?.windows[0]?.usedPercent).toBe(50);
+ });
+
+ it("keeps the freshest report of a credential that repeats", () => {
+ const goAccount = (key: string, checkedAt: string, usedPercent: number) => {
+ const windows = [
+ { ...weekly, id: "go_weekly", usedPercent, credentialFingerprint: "go-key" },
+ ];
+ return {
+ ...account(key, windows),
+ driver: ProviderDriverKind.make("opencode"),
+ limits: { checkedAt, windows },
+ };
+ };
+ const [pool] = collectLimitPools(
+ [
+ goAccount("env-a", "2026-09-03T10:00:00.000Z", 20),
+ goAccount("env-b", "2026-09-03T11:30:00.000Z", 80),
+ ],
+ now,
+ );
+ expect(pool?.windows[0]?.members).toHaveLength(1);
+ expect(pool?.windows[0]?.usedPercent).toBe(80);
+ });
+
it.each([
{
reports: [
diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts
index b7902f11d6733..1b40ea03fa492 100644
--- a/packages/shared/src/usageLimits.ts
+++ b/packages/shared/src/usageLimits.ts
@@ -66,10 +66,19 @@ export function exhaustedUsageWindow(
);
return exhausted.find((window) => window.id !== "individual" || !hasPlanWindows);
}
- case "opencode":
- return exhausted.find(
- (window) => !window.id.startsWith("go_") || model.startsWith("opencode-go/"),
- );
+ case "opencode": {
+ // Each window names the integration that meters it, so a model's limits
+ // come from its own provider instead of whichever plan happens to be
+ // exhausted. The Console budget is metered as "go" or "budget".
+ const integration = model.split("/")[0] ?? "";
+ return exhausted.find((window) => {
+ const owner = window.id.split("_")[0]!;
+ if (owner === "go") return integration === "opencode-go";
+ // A Console workspace budget covers every model in the workspace.
+ if (owner === "budget") return true;
+ return owner === integration;
+ });
+ }
default:
return exhausted[0];
}
@@ -622,8 +631,26 @@ function poolWindows(accounts: readonly LimitAccount[], now: number): readonly L
? `${window.kind}:${window.id}`
: pooled;
const list = byKey.get(key);
- if (list) list.push(member);
- else byKey.set(key, [member]);
+ if (list) {
+ // The same credential on two accounts is one allowance, so it counts
+ // once, and the fresher report of it is the one that counts.
+ const credential = window.credentialFingerprint;
+ const repeated =
+ credential === undefined
+ ? -1
+ : list.findIndex((member) => member.window.credentialFingerprint === credential);
+ if (repeated !== -1) {
+ const previous = list[repeated]!;
+ if (
+ Date.parse(member.account.limits.checkedAt) >
+ Date.parse(previous.account.limits.checkedAt)
+ ) {
+ list[repeated] = member;
+ }
+ continue;
+ }
+ list.push(member);
+ } else byKey.set(key, [member]);
}
}
const pools = [...byKey].map(([key, members]): LimitPoolWindow => {
diff --git a/packages/shared/src/usageMerge.test.ts b/packages/shared/src/usageMerge.test.ts
index 828b652077f62..ec2e31dd1709d 100644
--- a/packages/shared/src/usageMerge.test.ts
+++ b/packages/shared/src/usageMerge.test.ts
@@ -312,6 +312,61 @@ describe("mergeUsage", () => {
expect(merged.records).toBe(5);
expect(merged.sessions).toBe(1);
expect(merged.contributingEnvironments).toEqual(["old"]);
+
+ const attributed = environment("new", {
+ ...partial.summary,
+ buckets: partial.summary.buckets.map((entry) => ({
+ ...entry,
+ upstreamProvider: "anthropic",
+ })),
+ });
+ expect(mergeUsage([complete, attributed], USAGE_CONTRACT_VERSION).costUsd).toBe(10);
+
+ const attributedComplete = environment("old", {
+ ...complete.summary,
+ buckets: complete.summary.buckets.map((entry) => ({
+ ...entry,
+ upstreamProvider: "anthropic",
+ })),
+ });
+ expect(mergeUsage([attributedComplete, partial], USAGE_CONTRACT_VERSION).costUsd).toBe(10);
+ });
+
+ it("filters the reconciled contributions instead of the summaries", () => {
+ const source = { provider: "claude" as const, hostId: "mac", homePath: "/home/theo/.claude" };
+ const complete = environment("old", summary([bucket()], [source]));
+ const partialSummary = summary(
+ [
+ bucket({ sourcePath: source.homePath, costUsd: 4, records: 2 }),
+ bucket({
+ day: "2026-08-08" as UsageDay,
+ sourcePath: source.homePath,
+ costUsd: 3,
+ records: 1,
+ }),
+ ],
+ [{ ...source, distinctSessions: 2 }],
+ );
+ const attributed = environment("new", {
+ ...partialSummary,
+ readAt: "2026-08-08T01:00:00.000Z",
+ sources: partialSummary.sources.map((entry) => ({ ...entry, status: "partial" as const })),
+ buckets: partialSummary.buckets.map((entry) => ({
+ ...entry,
+ upstreamProvider: "anthropic",
+ })),
+ });
+
+ // The complete scan owns the overlapping day, so only its later day is the
+ // attributed model's own usage.
+ expect(mergeUsage([complete, attributed], USAGE_CONTRACT_VERSION).costUsd).toBe(13);
+ expect(
+ mergeUsage(
+ [complete, attributed],
+ USAGE_CONTRACT_VERSION,
+ (bucket) => bucket.upstreamProvider === "anthropic",
+ ).costUsd,
+ ).toBe(3);
});
it("identifies an environment reporting an older contract version", () => {
diff --git a/packages/shared/src/usageMerge.ts b/packages/shared/src/usageMerge.ts
index 423ccef4bbd63..723b46e98df88 100644
--- a/packages/shared/src/usageMerge.ts
+++ b/packages/shared/src/usageMerge.ts
@@ -28,6 +28,8 @@ export interface ProviderTotals {
export interface ModelTotals {
readonly model: string;
readonly provider: UsageProviderKind;
+ /** The upstream integration that served the model, when the harness knows it. */
+ readonly upstreamProvider?: string;
readonly costUsd: number;
readonly totalTokens: number;
readonly tokens: UsageTokenTotals;
@@ -130,8 +132,17 @@ function bucketsForSource(summary: UsageSummary, source: UsageSource): readonly
);
}
-function bucketKey(bucket: UsageBucket): string {
- return JSON.stringify([bucket.day, bucket.hourStart ?? null, bucket.provider, bucket.model]);
+function bucketKey(
+ bucket: UsageBucket,
+ upstreamProvider: string | null = bucket.upstreamProvider ?? null,
+): string {
+ return JSON.stringify([
+ bucket.day,
+ bucket.hourStart ?? null,
+ bucket.provider,
+ upstreamProvider,
+ bucket.model,
+ ]);
}
function claimSources(environments: readonly EnvironmentUsage[]): {
@@ -145,7 +156,17 @@ function claimSources(environments: readonly EnvironmentUsage[]): {
string,
{ environment: EnvironmentUsage; source: UsageSource }
>();
- const seenBucketKeysByFingerprint = new Map>();
+ const seenBucketKeysByFingerprint = new Map<
+ string,
+ {
+ /** The owner's cells, keyed by their own attribution. */
+ readonly attributed: Set;
+ /** The owner's cells that carry no attribution. */
+ readonly unattributed: Set;
+ /** The owner's cells, keyed without their attribution. */
+ readonly any: Set;
+ }
+ >();
const supplementalBucketsByEnvironment = new Map>();
const sessionsByFingerprint = new Map();
const duplicates: string[] = [];
@@ -189,7 +210,16 @@ function claimSources(environments: readonly EnvironmentUsage[]): {
}
let seen = seenBucketKeysByFingerprint.get(key);
if (seen === undefined) {
- seen = new Set(bucketsForSource(owner.environment.summary, owner.source).map(bucketKey));
+ const ownerCells = bucketsForSource(owner.environment.summary, owner.source);
+ seen = {
+ attributed: new Set(ownerCells.map((bucket) => bucketKey(bucket))),
+ unattributed: new Set(
+ ownerCells
+ .filter((bucket) => bucket.upstreamProvider === undefined)
+ .map((bucket) => bucketKey(bucket, null)),
+ ),
+ any: new Set(ownerCells.map((bucket) => bucketKey(bucket, null))),
+ };
seenBucketKeysByFingerprint.set(key, seen);
}
const supplemental =
@@ -197,8 +227,18 @@ function claimSources(environments: readonly EnvironmentUsage[]): {
let added = false;
for (const bucket of bucketsForSource(environment.summary, source)) {
const cell = bucketKey(bucket);
- if (seen.has(cell)) continue;
- seen.add(cell);
+ const unattributedCell = bucketKey(bucket, null);
+ // A cell is already counted when the owner holds the same records: with
+ // the same attribution, without one (a server that did not know the
+ // upstream), or, for a cell without attribution, under any attribution.
+ if (
+ seen.attributed.has(cell) ||
+ seen.unattributed.has(unattributedCell) ||
+ (bucket.upstreamProvider === undefined && seen.any.has(unattributedCell))
+ )
+ continue;
+ seen.attributed.add(cell);
+ seen.any.add(unattributedCell);
supplemental.add(bucket);
added = true;
}
@@ -302,6 +342,7 @@ const EMPTY_MERGED: MergedUsage = {
export function mergeUsage(
environments: readonly EnvironmentUsage[],
expectedContractVersion: number,
+ keepBucket?: (bucket: UsageBucket) => boolean,
): MergedUsage {
if (environments.length === 0) return EMPTY_MERGED;
@@ -353,6 +394,8 @@ export function mergeUsage(
string,
{
provider: UsageProviderKind;
+ upstreamProvider?: string;
+ model: string;
costUsd: number;
totalTokens: number;
tokens: UsageTokenTotals;
@@ -388,7 +431,10 @@ export function mergeUsage(
supplementalBucketsByEnvironment.get(environment.environmentId) ?? new Set(),
sessionsByFingerprint,
);
- if (buckets.length > 0) contributingEnvironments.push(environment.environmentId);
+ // The filter runs on the reconciled contributions, so a cell that only a
+ // newer scan attributes still counts once.
+ const keptBuckets = keepBucket === undefined ? buckets : buckets.filter(keepBucket);
+ if (keptBuckets.length > 0) contributingEnvironments.push(environment.environmentId);
for (const [providerKind, providerSessions] of sessionsByProvider) {
sessions += providerSessions;
@@ -403,7 +449,7 @@ export function mergeUsage(
providerAccumulator.set(providerKind, provider);
}
- for (const bucket of buckets) {
+ for (const bucket of keptBuckets) {
const tokens = bucketTokens(bucket);
costUsd += bucket.costUsd;
@@ -437,9 +483,11 @@ export function mergeUsage(
provider.records += bucket.records;
providerAccumulator.set(bucket.provider, provider);
- const modelKey = `${bucket.provider} ${bucket.model}`;
+ const modelKey = `${bucket.provider} ${bucket.upstreamProvider ?? ""} ${bucket.model}`;
const model = modelAccumulator.get(modelKey) ?? {
provider: bucket.provider,
+ ...(bucket.upstreamProvider ? { upstreamProvider: bucket.upstreamProvider } : {}),
+ model: bucket.model,
costUsd: 0,
totalTokens: 0,
tokens: {
@@ -521,9 +569,10 @@ export function mergeUsage(
.sort((a, b) => b.costUsd - a.costUsd);
const models: ModelTotals[] = [...modelAccumulator.entries()]
- .map(([key, totals]) => ({
- model: key.slice(key.indexOf(" ") + 1),
+ .map(([, totals]) => ({
+ model: totals.model,
provider: totals.provider,
+ ...(totals.upstreamProvider ? { upstreamProvider: totals.upstreamProvider } : {}),
costUsd: totals.costUsd,
totalTokens: totals.totalTokens,
tokens: totals.tokens,