fix(usage): restore commandcode quota + timeout guards lost in master merge

The -X theirs merge of origin/master (v0.5.69) silently reverted five
branch-only hunks because upstream had no conflict-region counterpart
and simply won the three-way pick:

* services/usage.js: re-register the commandcode USAGE handler +
  import. Without it the dashboard Quota Tracker fell through to
  'Usage API not implemented for commandcode'. The handler module
  (services/usage/commandcode.js) and registry usage block survived;
  only the dispatch entry was dropped.
* ProviderLimits/utils.js: restore parseQuotaData 'commandcode' case
  (currency-credit rows need unit "$" + remainingPercentage
  forwarding, else $0.05 balances render as 0%).
* profile + providers/[id] pages: restore Math.max(1000, ...) connect
  timeout floors so a stray '60' is never interpreted as 60ms.
* .gitignore: re-add .commandcode/ CLI local state.

Verified: tests/unit/commandcode-usage.test.js (6) and
usage-dispatch.test.js (2, asserts every provider routes to a real
handler) pass standalone; full unit run 1742 pass / 107 fail vs
1738/111 before this fix (remaining failures pre-existing, unrelated).
This commit is contained in:
2026-09-08 09:28:59 +07:00
parent ddfa789a31
commit e3ba5d2207
5 changed files with 31 additions and 4 deletions

3
.gitignore vendored
View File

@@ -89,3 +89,6 @@ graphify-out/*
# Kiro local workspace state
.kiro/
# CommandCode CLI local state (auth/taste/projects)
.commandcode/

View File

@@ -15,6 +15,7 @@ import { getXaiUsage } from "./usage/xai.js";
import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getKimiUsage } from "./usage/kimi.js";
import { getDeepseekUsage } from "./usage/deepseek.js";
import { getCommandCodeUsage } from "./usage/commandcode.js";
import { getOpenCodeGoUsage } from "./usage/opencode-go.js";
import { getGroqUsage } from "./usage/groq.js";
import { getZedUsage } from "./usage/zed.js";
@@ -60,6 +61,7 @@ const USAGE_HANDLERS = {
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
"opencode-go": (c) => getOpenCodeGoUsage(c.apiKey, c.proxyOptions),
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
commandcode: (c) => getCommandCodeUsage(c.apiKey, c.proxyOptions),
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
};

View File

@@ -295,7 +295,9 @@ export default function ProfilePage() {
const handleGlobalTimeoutChange = async (e) => {
const raw = e.target.value.replace(/[^0-9]/g, "");
const numTimeout = parseInt(raw, 10);
const patchValue = (raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0) ? numTimeout : null;
// Enforce a sane minimum (1s) so a stray "60" never becomes a 60ms
// connect timeout — same guard as the per-provider timeout input.
const patchValue = (raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0) ? Math.max(1000, numTimeout) : null;
try {
const res = await fetch("/api/settings", {

View File

@@ -326,7 +326,7 @@ export default function ProviderDetailPage() {
setProviderStrategy(override.fallbackStrategy || null);
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
setProviderNoAuthEnabled(override.enabled !== false);
// Load per-provider connect timeout
// Load per-provider connect timeout (ms)
const timeoutCfg = (settingsData.providerTimeouts || {})[providerId] || {};
setProviderTimeout(timeoutCfg.timeoutMs != null ? String(timeoutCfg.timeoutMs) : "");
// Load per-provider thinking config
@@ -478,8 +478,10 @@ export default function ProviderDetailPage() {
delete updated[providerId];
} else {
const timeoutMs = parseInt(ms, 10);
// Stored in ms; enforce a sane minimum (1s) so a stray "60" means
// 60 SECONDS-worth of protection — never a 60ms connect timeout.
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
updated[providerId] = { timeoutMs };
updated[providerId] = { timeoutMs: Math.max(1000, timeoutMs) };
} else {
delete updated[providerId];
}

View File

@@ -648,6 +648,24 @@ export function parseQuotaData(provider, data) {
}
break;
case "commandcode":
// CommandCode reports currency credits (5-hour/weekly windows + monthly
// credits) with used/total in dollars. Forward remainingPercentage (the
// UI would otherwise render "$0.05" balances as "0%") and unit "$".
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]) => {
normalizedQuotas.push({
name,
used: quota.used || 0,
total: quota.total || 0,
unit: quota.unit || "$",
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
}
break;
default:
// Generic fallback for unknown providers
if (data.quotas) {