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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/electron-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ jobs:

- name: Install dependencies
run: npm ci
env:
NPM_CONFIG_LEGACY_PEER_DEPS: true

- name: Sanitize Windows home directory
if: runner.os == 'Windows'
shell: bash
run: |
# The default USERPROFILE contains junction points (Application Data)
# that cause EPERM errors during Next.js standalone build glob scans.
# Create a clean temp profile directory to avoid this.
mkdir -p "$RUNNER_TEMP/home"
echo "USERPROFILE=$RUNNER_TEMP/home" >> $GITHUB_ENV

- name: Build Next.js standalone
env:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ COPY package*.json ./
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
COPY scripts/postinstallSupport.mjs ./scripts/postinstallSupport.mjs
COPY scripts/native-binary-compat.mjs ./scripts/native-binary-compat.mjs
COPY scripts/postinstallSupport.mjs ./scripts/postinstallSupport.mjs
ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi

COPY . ./
Expand Down
3 changes: 2 additions & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"electron-builder": "^26.8.1"
},
"overrides": {
"@xmldom/xmldom": "^0.9.10"
"@xmldom/xmldom": "^0.9.10",
"plist": "^4.0.0"
},
"build": {
"appId": "online.omniroute.desktop",
Expand Down
29 changes: 21 additions & 8 deletions open-sse/services/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ function pickMiniMaxRepresentativeModel(
return pool.reduce((best, current) => (getTotal(current) > getTotal(best) ? current : best));
}

function createMiniMaxQuotaFromCount(
total: number,
count: number,
resetAt: string | null,
countMeansRemaining: boolean
): UsageQuota {
const used = countMeansRemaining ? Math.max(total - count, 0) : count;
return createQuotaFromUsage(used, total, resetAt);
}

function getMiniMaxAuthErrorMessage(message: string): string {
const normalized = message.toLowerCase();
if (
Expand Down Expand Up @@ -319,11 +329,12 @@ async function getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn
return { message: "MiniMax connected. No text quota data was returned." };
}

const countMeansRemaining = usageUrl.includes("/coding_plan/remains");
const quotas: Record<string, UsageQuota> = {};
const sessionModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxSessionTotal);
if (sessionModel) {
const total = getMiniMaxSessionTotal(sessionModel);
const remain = Math.max(
const count = Math.max(
0,
toNumber(
getFieldValue(
Expand All @@ -334,41 +345,43 @@ async function getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn
0
)
);
quotas["session (5h)"] = createQuotaFromUsage(
Math.max(total - remain, 0),
quotas["session (5h)"] = createMiniMaxQuotaFromCount(
total,
count,
getMiniMaxQuotaResetAt(
sessionModel,
capturedAtMs,
"remains_time",
"remainsTime",
"end_time",
"endTime"
)
),
countMeansRemaining
);
}

const weeklyModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxWeeklyTotal);
if (weeklyModel && getMiniMaxWeeklyTotal(weeklyModel) > 0) {
const total = getMiniMaxWeeklyTotal(weeklyModel);
const remain = Math.max(
const count = Math.max(
0,
toNumber(
getFieldValue(weeklyModel, "current_weekly_usage_count", "currentWeeklyUsageCount"),
0
)
);
quotas["weekly (7d)"] = createQuotaFromUsage(
Math.max(total - remain, 0),
quotas["weekly (7d)"] = createMiniMaxQuotaFromCount(
total,
count,
getMiniMaxQuotaResetAt(
weeklyModel,
capturedAtMs,
"weekly_remains_time",
"weeklyRemainsTime",
"weekly_end_time",
"weeklyEndTime"
)
),
countMeansRemaining
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,10 @@ export default function ProviderLimits() {
<div className="text-xs text-text-muted italic">{quota.message}</div>
) : quota?.quotas?.length > 0 ? (
quota.quotas.map((q, i) => {
const remainingPercentage = q.unlimited
const remainingPercentageRaw = q.unlimited
? 100
: (q.remainingPercentage ?? calculatePercentage(q.used, q.total));
const remainingPercentage = Math.round(remainingPercentageRaw);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Rounding the remaining percentage to the nearest integer can lead to misleading displays at the extremes. For example, a quota with 99.6% remaining will be displayed as 100%, which might incorrectly suggest that no usage has occurred. Conversely, a quota with 0.4% remaining will show as 0%, suggesting it is completely exhausted.

Consider using a more precise rounding method (such as toFixed(1) for values near the boundaries) or using Math.floor for the remaining percentage to ensure that 100% is only shown when the quota is truly untouched, maintaining better accuracy for the user.

const colors = getBarColor(remainingPercentage);
const cd = formatCountdown(q.resetAt);
const shortName = formatQuotaLabel(q.name);
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/usage-service-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,46 @@ test("usage service covers MiniMax usage parsing, documented endpoint fallback a
assert.match(invalid.message, /Token Plan API key/i);
});

test("usage service treats MiniMax token-plan counts as used usage", async () => {
const beforeCall = Date.now();

globalThis.fetch = async (url, init = {}) => {
assert.equal(String(url), "https://www.minimax.io/v1/token_plan/remains");
assert.equal((init as any).headers.Authorization, "Bearer minimax-key");

return new Response(
JSON.stringify({
base_resp: { status_code: 0, status_msg: "ok" },
model_remains: [
{
model_name: "MiniMax-M2.7",
remains_time: 300_000,
current_interval_total_count: 15000,
current_interval_usage_count: 13,
current_weekly_total_count: 150000,
current_weekly_usage_count: 66,
weekly_remains_time: 604_800_000,
},
],
}),
{ status: 200 }
);
};

const usage: any = await usageService.getUsageForProvider({
provider: "minimax",
apiKey: "minimax-key",
});

assert.equal(usage.quotas["session (5h)"].used, 13);
assert.equal(usage.quotas["session (5h)"].remaining, 14987);
assert.equal(usage.quotas["session (5h)"].remainingPercentage, 99.91333333333333);
assert.equal(usage.quotas["weekly (7d)"].used, 66);
assert.equal(usage.quotas["weekly (7d)"].remaining, 149934);
assert.equal(usage.quotas["weekly (7d)"].remainingPercentage, 99.956);
assert.ok(Date.parse(usage.quotas["session (5h)"].resetAt) >= beforeCall + 240_000);
});

test("usage service parses Cursor team quotas and clamps on-demand ratio", async () => {
const calls: any[] = [];
globalThis.fetch = async (url, init = {}) => {
Expand Down