mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:04:32 +00:00
refactor: dedupe provider usage auth/fetch logic and expand coverage
This commit is contained in:
@@ -74,6 +74,39 @@ describe("resolveProviderAuths key normalization", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeConfig(home: string, config: Record<string, unknown>) {
|
||||||
|
const stateDir = path.join(home, ".openclaw");
|
||||||
|
await fs.mkdir(stateDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(stateDir, "openclaw.json"),
|
||||||
|
`${JSON.stringify(config, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeProfileOrder(home: string, provider: string, profileIds: string[]) {
|
||||||
|
const agentDir = path.join(home, ".openclaw", "agents", "main", "agent");
|
||||||
|
const parsed = JSON.parse(
|
||||||
|
await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"),
|
||||||
|
) as Record<string, unknown>;
|
||||||
|
const order = (parsed.order && typeof parsed.order === "object" ? parsed.order : {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
order[provider] = profileIds;
|
||||||
|
parsed.order = order;
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(agentDir, "auth-profiles.json"),
|
||||||
|
`${JSON.stringify(parsed, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeLegacyPiAuth(home: string, raw: string) {
|
||||||
|
const legacyDir = path.join(home, ".pi", "agent");
|
||||||
|
await fs.mkdir(legacyDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(legacyDir, "auth.json"), raw, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
it("strips embedded CR/LF from env keys", async () => {
|
it("strips embedded CR/LF from env keys", async () => {
|
||||||
await withSuiteHome(
|
await withSuiteHome(
|
||||||
async () => {
|
async () => {
|
||||||
@@ -144,12 +177,9 @@ describe("resolveProviderAuths key normalization", () => {
|
|||||||
it("falls back to legacy .pi auth file for zai keys", async () => {
|
it("falls back to legacy .pi auth file for zai keys", async () => {
|
||||||
await withSuiteHome(
|
await withSuiteHome(
|
||||||
async (home) => {
|
async (home) => {
|
||||||
const legacyDir = path.join(home, ".pi", "agent");
|
await writeLegacyPiAuth(
|
||||||
await fs.mkdir(legacyDir, { recursive: true });
|
home,
|
||||||
await fs.writeFile(
|
|
||||||
path.join(legacyDir, "auth.json"),
|
|
||||||
`${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`,
|
`${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`,
|
||||||
"utf8",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const auths = await resolveProviderAuths({
|
const auths = await resolveProviderAuths({
|
||||||
@@ -180,4 +210,195 @@ describe("resolveProviderAuths key normalization", () => {
|
|||||||
expect(auths).toEqual([{ provider: "google-gemini-cli", token: "google-oauth-token" }]);
|
expect(auths).toEqual([{ provider: "google-gemini-cli", token: "google-oauth-token" }]);
|
||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps raw google token when token payload is not JSON", async () => {
|
||||||
|
await withSuiteHome(async (home) => {
|
||||||
|
await writeAuthProfiles(home, {
|
||||||
|
"google-antigravity:default": {
|
||||||
|
type: "token",
|
||||||
|
provider: "google-antigravity",
|
||||||
|
token: "plain-google-token",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["google-antigravity"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([{ provider: "google-antigravity", token: "plain-google-token" }]);
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses config api keys when env and profiles are missing", async () => {
|
||||||
|
await withSuiteHome(
|
||||||
|
async (home) => {
|
||||||
|
const modelDef = {
|
||||||
|
id: "test-model",
|
||||||
|
name: "Test Model",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 1024,
|
||||||
|
maxTokens: 256,
|
||||||
|
};
|
||||||
|
await writeConfig(home, {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
zai: {
|
||||||
|
baseUrl: "https://api.z.ai",
|
||||||
|
models: [modelDef],
|
||||||
|
apiKey: "cfg-zai-key",
|
||||||
|
},
|
||||||
|
minimax: {
|
||||||
|
baseUrl: "https://api.minimaxi.com",
|
||||||
|
models: [modelDef],
|
||||||
|
apiKey: "cfg-minimax-key",
|
||||||
|
},
|
||||||
|
xiaomi: {
|
||||||
|
baseUrl: "https://api.xiaomi.example",
|
||||||
|
models: [modelDef],
|
||||||
|
apiKey: "cfg-xiaomi-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["zai", "minimax", "xiaomi"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([
|
||||||
|
{ provider: "zai", token: "cfg-zai-key" },
|
||||||
|
{ provider: "minimax", token: "cfg-minimax-key" },
|
||||||
|
{ provider: "xiaomi", token: "cfg-xiaomi-key" },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZAI_API_KEY: undefined,
|
||||||
|
Z_AI_API_KEY: undefined,
|
||||||
|
MINIMAX_API_KEY: undefined,
|
||||||
|
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||||
|
XIAOMI_API_KEY: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no auth when providers have no configured credentials", async () => {
|
||||||
|
await withSuiteHome(
|
||||||
|
async () => {
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["zai", "minimax", "xiaomi"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([]);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZAI_API_KEY: undefined,
|
||||||
|
Z_AI_API_KEY: undefined,
|
||||||
|
MINIMAX_API_KEY: undefined,
|
||||||
|
MINIMAX_CODE_PLAN_KEY: undefined,
|
||||||
|
XIAOMI_API_KEY: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses zai api_key auth profiles when env and config are missing", async () => {
|
||||||
|
await withSuiteHome(
|
||||||
|
async (home) => {
|
||||||
|
await writeAuthProfiles(home, {
|
||||||
|
"zai:default": { type: "api_key", provider: "zai", key: "profile-zai-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["zai"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([{ provider: "zai", token: "profile-zai-key" }]);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZAI_API_KEY: undefined,
|
||||||
|
Z_AI_API_KEY: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid legacy z-ai auth files", async () => {
|
||||||
|
await withSuiteHome(
|
||||||
|
async (home) => {
|
||||||
|
await writeLegacyPiAuth(home, "{not-json");
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["zai"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([]);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ZAI_API_KEY: undefined,
|
||||||
|
Z_AI_API_KEY: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("discovers oauth provider from config but skips mismatched profile providers", async () => {
|
||||||
|
await withSuiteHome(async (home) => {
|
||||||
|
await writeConfig(home, {
|
||||||
|
auth: {
|
||||||
|
profiles: {
|
||||||
|
"anthropic:default": { provider: "anthropic", mode: "token" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await writeAuthProfiles(home, {
|
||||||
|
"anthropic:default": {
|
||||||
|
type: "token",
|
||||||
|
provider: "zai",
|
||||||
|
token: "mismatched-provider-token",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["anthropic"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([]);
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips providers without oauth-compatible profiles", async () => {
|
||||||
|
await withSuiteHome(async () => {
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["anthropic"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([]);
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips oauth profiles that resolve without an api key and uses later profiles", async () => {
|
||||||
|
await withSuiteHome(async (home) => {
|
||||||
|
await writeAuthProfiles(home, {
|
||||||
|
"anthropic:empty": {
|
||||||
|
type: "token",
|
||||||
|
provider: "anthropic",
|
||||||
|
token: "expired-token",
|
||||||
|
expires: Date.now() - 60_000,
|
||||||
|
},
|
||||||
|
"anthropic:valid": { type: "token", provider: "anthropic", token: "anthropic-token" },
|
||||||
|
});
|
||||||
|
await writeProfileOrder(home, "anthropic", ["anthropic:empty", "anthropic:valid"]);
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["anthropic"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([{ provider: "anthropic", token: "anthropic-token" }]);
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips api_key entries in oauth token resolution order", async () => {
|
||||||
|
await withSuiteHome(async (home) => {
|
||||||
|
await writeAuthProfiles(home, {
|
||||||
|
"anthropic:api": { type: "api_key", provider: "anthropic", key: "api-key-1" },
|
||||||
|
"anthropic:token": { type: "token", provider: "anthropic", token: "token-1" },
|
||||||
|
});
|
||||||
|
await writeProfileOrder(home, "anthropic", ["anthropic:api", "anthropic:token"]);
|
||||||
|
|
||||||
|
const auths = await resolveProviderAuths({
|
||||||
|
providers: ["anthropic"],
|
||||||
|
});
|
||||||
|
expect(auths).toEqual([{ provider: "anthropic", token: "token-1" }]);
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
resolveApiKeyForProfile,
|
resolveApiKeyForProfile,
|
||||||
resolveAuthProfileOrder,
|
resolveAuthProfileOrder,
|
||||||
} from "../agents/auth-profiles.js";
|
} from "../agents/auth-profiles.js";
|
||||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
|
import { getCustomProviderApiKey } from "../agents/model-auth.js";
|
||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
@@ -21,9 +21,6 @@ export type ProviderAuth = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseGoogleToken(apiKey: string): { token: string } | null {
|
function parseGoogleToken(apiKey: string): { token: string } | null {
|
||||||
if (!apiKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
||||||
if (parsed && typeof parsed.token === "string") {
|
if (parsed && typeof parsed.token === "string") {
|
||||||
@@ -42,11 +39,6 @@ function resolveZaiApiKey(): string | undefined {
|
|||||||
return envDirect;
|
return envDirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envResolved = resolveEnvApiKey("zai");
|
|
||||||
if (envResolved?.apiKey) {
|
|
||||||
return envResolved.apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const key = getCustomProviderApiKey(cfg, "zai") || getCustomProviderApiKey(cfg, "z-ai");
|
const key = getCustomProviderApiKey(cfg, "zai") || getCustomProviderApiKey(cfg, "z-ai");
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -103,11 +95,6 @@ function resolveProviderApiKeyFromConfigAndStore(params: {
|
|||||||
return envDirect;
|
return envDirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envResolved = resolveEnvApiKey(params.providerId);
|
|
||||||
if (envResolved?.apiKey) {
|
|
||||||
return envResolved.apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const key = getCustomProviderApiKey(cfg, params.providerId);
|
const key = getCustomProviderApiKey(cfg, params.providerId);
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -115,21 +102,23 @@ function resolveProviderApiKeyFromConfigAndStore(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const store = ensureAuthProfileStore();
|
const store = ensureAuthProfileStore();
|
||||||
const apiProfile = listProfilesForProvider(store, params.providerId).find((id) => {
|
const cred = listProfilesForProvider(store, params.providerId)
|
||||||
const cred = store.profiles[id];
|
.map((id) => store.profiles[id])
|
||||||
return cred?.type === "api_key" || cred?.type === "token";
|
.find(
|
||||||
});
|
(
|
||||||
if (!apiProfile) {
|
profile,
|
||||||
|
): profile is
|
||||||
|
| { type: "api_key"; provider: string; key: string }
|
||||||
|
| { type: "token"; provider: string; token: string } =>
|
||||||
|
profile?.type === "api_key" || profile?.type === "token",
|
||||||
|
);
|
||||||
|
if (!cred) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const cred = store.profiles[apiProfile];
|
if (cred.type === "api_key") {
|
||||||
if (cred?.type === "api_key") {
|
|
||||||
return normalizeSecretInput(cred.key);
|
return normalizeSecretInput(cred.key);
|
||||||
}
|
}
|
||||||
if (cred?.type === "token") {
|
return normalizeSecretInput(cred.token);
|
||||||
return normalizeSecretInput(cred.token);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveOAuthToken(params: {
|
async function resolveOAuthToken(params: {
|
||||||
@@ -161,22 +150,21 @@ async function resolveOAuthToken(params: {
|
|||||||
profileId,
|
profileId,
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (!resolved?.apiKey) {
|
if (resolved) {
|
||||||
continue;
|
let token = resolved.apiKey;
|
||||||
|
if (params.provider === "google-gemini-cli" || params.provider === "google-antigravity") {
|
||||||
|
const parsed = parseGoogleToken(resolved.apiKey);
|
||||||
|
token = parsed?.token ?? resolved.apiKey;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: params.provider,
|
||||||
|
token,
|
||||||
|
accountId:
|
||||||
|
cred.type === "oauth" && "accountId" in cred
|
||||||
|
? (cred as { accountId?: string }).accountId
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let token = resolved.apiKey;
|
|
||||||
if (params.provider === "google-gemini-cli" || params.provider === "google-antigravity") {
|
|
||||||
const parsed = parseGoogleToken(resolved.apiKey);
|
|
||||||
token = parsed?.token ?? resolved.apiKey;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
provider: params.provider,
|
|
||||||
token,
|
|
||||||
accountId:
|
|
||||||
cred.type === "oauth" && "accountId" in cred
|
|
||||||
? (cred as { accountId?: string }).accountId
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { logDebug } from "../logger.js";
|
import { logDebug } from "../logger.js";
|
||||||
import { fetchJson } from "./provider-usage.fetch.shared.js";
|
import { fetchJson, parseFiniteNumber } from "./provider-usage.fetch.shared.js";
|
||||||
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
||||||
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
||||||
|
|
||||||
@@ -46,16 +46,7 @@ const METADATA = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseNumber(value: number | string | undefined): number | undefined {
|
function parseNumber(value: number | string | undefined): number | undefined {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
return parseFiniteNumber(value);
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const parsed = Number.parseFloat(value);
|
|
||||||
if (Number.isFinite(parsed)) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEpochMs(isoString: string | undefined): number | undefined {
|
function parseEpochMs(isoString: string | undefined): number | undefined {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { isRecord } from "../utils.js";
|
import { isRecord } from "../utils.js";
|
||||||
import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js";
|
import {
|
||||||
|
buildUsageHttpErrorSnapshot,
|
||||||
|
fetchJson,
|
||||||
|
parseFiniteNumber,
|
||||||
|
} from "./provider-usage.fetch.shared.js";
|
||||||
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
|
||||||
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
|
||||||
|
|
||||||
@@ -151,15 +155,9 @@ const WINDOW_MINUTE_KEYS = [
|
|||||||
|
|
||||||
function pickNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined {
|
function pickNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = record[key];
|
const parsed = parseFiniteNumber(record[key]);
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (parsed !== undefined) {
|
||||||
return value;
|
return parsed;
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const parsed = Number.parseFloat(value);
|
|
||||||
if (Number.isFinite(parsed)) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -16,6 +16,19 @@ export async function fetchJson(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseFiniteNumber(value: unknown): number | undefined {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
type BuildUsageHttpErrorSnapshotOptions = {
|
type BuildUsageHttpErrorSnapshotOptions = {
|
||||||
provider: UsageProviderId;
|
provider: UsageProviderId;
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ function formatResetRemaining(targetMs?: number, now?: number): string | null {
|
|||||||
}).format(new Date(targetMs));
|
}).format(new Date(targetMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickPrimaryWindow(windows: UsageWindow[]): UsageWindow | undefined {
|
|
||||||
if (windows.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return windows.reduce((best, next) => (next.usedPercent > best.usedPercent ? next : best));
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWindowShort(window: UsageWindow, now?: number): string {
|
function formatWindowShort(window: UsageWindow, now?: number): string {
|
||||||
const remaining = clampPercent(100 - window.usedPercent);
|
const remaining = clampPercent(100 - window.usedPercent);
|
||||||
const reset = formatResetRemaining(window.resetAt, now);
|
const reset = formatResetRemaining(window.resetAt, now);
|
||||||
@@ -84,19 +77,12 @@ export function formatUsageSummaryLine(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = providers
|
const parts = providers.map((entry) => {
|
||||||
.map((entry) => {
|
const window = entry.windows.reduce((best, next) =>
|
||||||
const window = pickPrimaryWindow(entry.windows);
|
next.usedPercent > best.usedPercent ? next : best,
|
||||||
if (!window) {
|
);
|
||||||
return null;
|
return `${entry.displayName} ${formatWindowShort(window, opts?.now)}`;
|
||||||
}
|
});
|
||||||
return `${entry.displayName} ${formatWindowShort(window, opts?.now)}`;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as string[];
|
|
||||||
|
|
||||||
if (parts.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `📊 Usage: ${parts.join(" · ")}`;
|
return `📊 Usage: ${parts.join(" · ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user